WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
@@ -0,0 +1,144 @@
|
||||
# 内置 MCP 服务管理指南
|
||||
|
||||
## 概述
|
||||
|
||||
内置 MCP 服务是系统级别的 MCP(Model Context Protocol)服务配置,对所有空间可见,但敏感信息会被隐藏,且不可编辑或删除。内置 MCP 服务通常用于提供系统默认的外部工具和资源接入,确保所有空间都能使用统一的 MCP 服务。
|
||||
|
||||
## 内置 MCP 服务特性
|
||||
|
||||
- **所有空间可见**:内置 MCP 服务对所有空间都可见,无需单独配置
|
||||
- **安全保护**:内置 MCP 服务的敏感信息(URL、认证配置、Headers、环境变量)会被隐藏,无法查看详情
|
||||
- **只读保护**:内置 MCP 服务不能被编辑或删除,仅支持测试连接
|
||||
- **统一管理**:由系统管理员统一维护,确保配置一致性和安全性
|
||||
|
||||
## 与内置模型的对比
|
||||
|
||||
| 特性 | 内置模型 | 内置 MCP 服务 |
|
||||
|------|---------|--------------|
|
||||
| 标识字段 | `is_builtin` | `is_builtin` |
|
||||
| 可见范围 | 所有空间 | 所有空间 |
|
||||
| 隐藏信息 | API Key、Base URL | URL、认证配置、Headers、环境变量 |
|
||||
| 编辑保护 | 不可编辑/删除 | 不可编辑/删除 |
|
||||
| 前端标签 | 显示"内置"标签 | 显示"内置"标签 |
|
||||
| 启停控制 | — | 禁用开关(始终启用) |
|
||||
|
||||
## 如何添加内置 MCP 服务
|
||||
|
||||
内置 MCP 服务需要通过数据库直接插入。以下是添加内置 MCP 服务的步骤:
|
||||
|
||||
### 1. 准备服务数据
|
||||
|
||||
首先,确保你已经有了要设置为内置 MCP 服务的配置信息,包括:
|
||||
- 服务名称(name)
|
||||
- 服务描述(description)
|
||||
- 传输方式(transport_type):`sse` 或 `http-streamable`
|
||||
- 服务地址(url):SSE / HTTP Streamable 必填
|
||||
- 认证配置(auth_config):可选,包括 api_key、token 等
|
||||
- 高级配置(advanced_config):可选,包括超时、重试策略等
|
||||
- 空间ID(tenant_id):建议使用小于 10000 的空间ID,避免冲突
|
||||
|
||||
**支持的传输方式**:
|
||||
- `sse`:Server-Sent Events,推荐用于流式体验
|
||||
- `http-streamable`:HTTP Streamable,标准 HTTP 兼容
|
||||
|
||||
> 注意:出于安全考虑,`stdio` 传输方式在服务端已被禁用。
|
||||
|
||||
### 2. 执行 SQL 插入语句
|
||||
|
||||
使用以下 SQL 语句插入内置 MCP 服务:
|
||||
|
||||
```sql
|
||||
-- 示例:插入一个 SSE 传输方式的内置 MCP 服务
|
||||
INSERT INTO mcp_services (
|
||||
id,
|
||||
tenant_id,
|
||||
name,
|
||||
description,
|
||||
enabled,
|
||||
transport_type,
|
||||
url,
|
||||
auth_config,
|
||||
advanced_config,
|
||||
is_builtin
|
||||
) VALUES (
|
||||
'builtin-mcp-001', -- 使用固定ID,建议使用 builtin-mcp- 前缀
|
||||
10000, -- 空间ID(使用第一个空间)
|
||||
'Web Search', -- 服务名称
|
||||
'内置 Web 搜索 MCP 服务', -- 描述
|
||||
true, -- 启用状态
|
||||
'sse', -- 传输方式
|
||||
'https://mcp.example.com/sse', -- 服务地址
|
||||
'{"api_key": "your-api-key"}'::jsonb, -- 认证配置
|
||||
'{"timeout": 30, "retry_count": 3, "retry_delay": 1}'::jsonb, -- 高级配置
|
||||
true -- 标记为内置服务
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 示例:插入一个 HTTP Streamable 传输方式的内置 MCP 服务
|
||||
INSERT INTO mcp_services (
|
||||
id,
|
||||
tenant_id,
|
||||
name,
|
||||
description,
|
||||
enabled,
|
||||
transport_type,
|
||||
url,
|
||||
headers,
|
||||
auth_config,
|
||||
advanced_config,
|
||||
is_builtin
|
||||
) VALUES (
|
||||
'builtin-mcp-002',
|
||||
10000,
|
||||
'Code Interpreter',
|
||||
'内置代码解释器 MCP 服务',
|
||||
true,
|
||||
'http-streamable',
|
||||
'https://mcp.example.com/stream',
|
||||
'{"X-Custom-Header": "value"}'::jsonb,
|
||||
'{"token": "your-bearer-token"}'::jsonb,
|
||||
'{"timeout": 60, "retry_count": 2, "retry_delay": 2}'::jsonb,
|
||||
true
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
```
|
||||
|
||||
### 3. 验证插入结果
|
||||
|
||||
执行以下 SQL 查询验证内置 MCP 服务是否成功插入:
|
||||
|
||||
```sql
|
||||
SELECT id, name, transport_type, enabled, is_builtin
|
||||
FROM mcp_services
|
||||
WHERE is_builtin = true
|
||||
ORDER BY created_at;
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **ID 命名规范**:建议使用 `builtin-mcp-{序号}` 的格式,例如 `builtin-mcp-001`、`builtin-mcp-002`
|
||||
2. **空间ID**:内置 MCP 服务可以属于任意空间,但建议使用第一个空间ID(通常是 10000)
|
||||
3. **JSON 格式**:`auth_config`、`advanced_config`、`headers` 等字段必须是有效的 JSON 格式
|
||||
4. **幂等性**:使用 `ON CONFLICT (id) DO NOTHING` 确保重复执行不会报错
|
||||
5. **安全性**:内置 MCP 服务的 URL、认证信息在前端会被自动隐藏,但数据库中的原始数据仍然存在,请妥善保管数据库访问权限
|
||||
6. **传输方式限制**:仅支持 `sse` 和 `http-streamable`,`stdio` 已被禁用
|
||||
|
||||
## 将现有 MCP 服务设置为内置服务
|
||||
|
||||
如果你已经有一个 MCP 服务,想将其设置为内置服务,可以使用 UPDATE 语句:
|
||||
|
||||
```sql
|
||||
UPDATE mcp_services
|
||||
SET is_builtin = true
|
||||
WHERE id = '服务ID' AND name = '服务名称';
|
||||
```
|
||||
|
||||
## 移除内置 MCP 服务
|
||||
|
||||
如果需要移除内置标记(恢复为普通 MCP 服务),执行:
|
||||
|
||||
```sql
|
||||
UPDATE mcp_services
|
||||
SET is_builtin = false
|
||||
WHERE id = '服务ID';
|
||||
```
|
||||
|
||||
注意:移除内置标记后,该 MCP 服务将恢复为普通服务,可以被编辑和删除。
|
||||
@@ -0,0 +1,282 @@
|
||||
# 内置模型管理指南
|
||||
|
||||
## 概述
|
||||
|
||||
内置模型是系统级别的模型配置,对所有空间可见。普通空间用户(包括空间管理员)看到的敏感信息会被隐藏,且不能修改;系统管理员可以在「模型管理」界面编辑配置和凭据。内置模型通常用于提供系统默认的模型配置,确保所有空间都能使用统一的模型服务。
|
||||
|
||||
## 内置模型特性
|
||||
|
||||
- **所有空间可见**:内置模型对所有空间都可见,无需单独配置
|
||||
- **安全保护**:API Key 等凭据永远不会明文返回;只有系统管理员能看到 Base URL 等管理信息和凭据是否已配置
|
||||
- **权限保护**:普通空间用户只能查看;系统管理员可以编辑配置和凭据
|
||||
- **统一管理**:内置模型对所有空间生效;删除仍通过 YAML 或 SQL 管理,避免部署配置与运行时状态冲突
|
||||
|
||||
## 在管理界面编辑
|
||||
|
||||
系统管理员可直接在「设置 → 模型管理」中点击内置模型,修改模型参数或更新凭据。普通空间管理员仍然只能查看。
|
||||
|
||||
首次通过管理界面保存 YAML 托管的内置模型后,该行会转为运行时托管(`managed_by` 清空),后续应用启动不会再用同 ID 的 YAML 条目覆盖它。这样可以保证界面保存的结果在重启后仍然有效。若希望重新交给 YAML 管理,需要由部署管理员在数据库中把该行的 `managed_by` 恢复为 `yaml`,或移除运行时覆盖后重新启动。
|
||||
|
||||
## 如何添加内置模型
|
||||
|
||||
WeKnora 支持两种方式添加内置模型:**推荐**使用 YAML 声明式配置(自动幂等下发),保留 SQL 直插作为兼容路径。
|
||||
|
||||
### 方式一(推荐):YAML 配置文件
|
||||
|
||||
#### 文件位置
|
||||
|
||||
默认路径是 `config/builtin_models.yaml`(与 `config.yaml`、`builtin_agents.yaml` 同目录)。如需挂载到其他位置,设置环境变量 `BUILTIN_MODELS_CONFIG=/absolute/path/builtin_models.yaml` 覆盖。
|
||||
|
||||
文件不存在时启动期会跳过、不报错;解析失败仅记录 Warning、不影响主流程。每次应用启动会重新读取并按 `id` 字段 UPSERT 到 `models` 表(保留 `created_at`,刷新其他字段)。如果同 ID 模型已经由系统管理员在界面保存并转为运行时托管,则保留运行时配置,不再由 YAML 覆盖。
|
||||
|
||||
#### Schema
|
||||
|
||||
```yaml
|
||||
builtin_models:
|
||||
- id: <required, stable, UPSERT key>
|
||||
tenant_id: <int, default 10000> # 与 tenants_id_seq 起点对齐
|
||||
name: <string>
|
||||
type: KnowledgeQA | Embedding | Rerank | VLLM | ASR
|
||||
source: <string, default "remote"> # local | remote | aliyun | ...
|
||||
description: <string, optional>
|
||||
is_default: <bool, default false>
|
||||
status: <string, default "active">
|
||||
parameters:
|
||||
base_url: <string>
|
||||
api_key: <string, supports ${ENV_VAR}>
|
||||
provider: <string> # openai | generic | moonshot | ...
|
||||
context_window: <int, optional> # 对话/VLM 上下文窗口(token);省略则默认 200000
|
||||
embedding_parameters: # 仅 Embedding 类型
|
||||
dimension: <int>
|
||||
truncate_prompt_tokens: <int>
|
||||
```
|
||||
|
||||
#### 完整示例
|
||||
|
||||
```yaml
|
||||
builtin_models:
|
||||
- id: builtin-openai-chat
|
||||
name: gpt-4o-mini
|
||||
type: KnowledgeQA
|
||||
source: remote
|
||||
description: OpenAI 默认对话模型
|
||||
is_default: false
|
||||
parameters:
|
||||
base_url: https://api.openai.com/v1
|
||||
api_key: ${OPENAI_API_KEY}
|
||||
provider: openai
|
||||
context_window: 200000
|
||||
|
||||
- id: builtin-openai-embeddings
|
||||
name: text-embedding-3-small
|
||||
type: Embedding
|
||||
source: remote
|
||||
parameters:
|
||||
base_url: https://api.openai.com/v1
|
||||
api_key: ${OPENAI_API_KEY}
|
||||
provider: openai
|
||||
embedding_parameters:
|
||||
dimension: 1536
|
||||
truncate_prompt_tokens: 0
|
||||
|
||||
- id: builtin-rerank
|
||||
name: bge-reranker-v2-m3
|
||||
type: Rerank
|
||||
source: remote
|
||||
parameters:
|
||||
base_url: ${RERANK_BASE_URL}
|
||||
api_key: ${RERANK_API_KEY}
|
||||
provider: generic
|
||||
```
|
||||
|
||||
#### `${ENV}` 插值
|
||||
|
||||
`api_key` / `base_url` / `name` 等任何**字符串**字段都可以引用环境变量:`${OPENAI_API_KEY}` 会在启动时被对应的 `os.Getenv("OPENAI_API_KEY")` 替换。
|
||||
|
||||
- 环境变量存在 → 替换为实际值
|
||||
- 环境变量不存在 → **保留字面 `${OPENAI_API_KEY}`** 字符串(让 401 错误能直接看出来 env 没设,便于排查)
|
||||
- 不支持 `${VAR:-default}` 这种 shell 扩展,行为与现有 `config.yaml` 的插值实现一致
|
||||
- **非字符串字段不能 env 化**(如 `type`、`dimension`、`is_default`),因为它们必须按 YAML 的目标类型解析
|
||||
|
||||
#### env 变量怎么进入容器
|
||||
|
||||
`docker-compose.yml` 的 `app` 服务已经预置了:
|
||||
|
||||
```yaml
|
||||
env_file:
|
||||
- path: .env
|
||||
required: false
|
||||
```
|
||||
|
||||
意味着把变量值写到项目根目录的 `.env` 文件里,启动时自动透传到容器。**无需**在 `environment:` 块里逐个透传。`required: false` 保证 `.env` 不存在时容器仍可启动(适配上游 fresh clone 场景)。
|
||||
|
||||
仓库的 `.env.example` 顶部预留了 **Built-in Models** 注释段,列出 LLM / Embedding / Rerank 的参考变量名作为起点;复制 `.env.example` 为 `.env` 后取消注释并填值即可。变量名由 YAML 自行决定,参考段只是常见样板,不是保留字。
|
||||
|
||||
完整端到端示例:
|
||||
|
||||
`.env`
|
||||
```bash
|
||||
LLM_MODEL_NAME=gpt-4o-mini
|
||||
LLM_BASE_URL=https://api.openai.com/v1
|
||||
LLM_API_KEY=sk-...
|
||||
LLM_PROVIDER=openai
|
||||
```
|
||||
|
||||
`config/builtin_models.yaml`
|
||||
```yaml
|
||||
builtin_models:
|
||||
- id: builtin-llm-default
|
||||
type: KnowledgeQA
|
||||
is_default: true
|
||||
name: ${LLM_MODEL_NAME}
|
||||
parameters:
|
||||
base_url: ${LLM_BASE_URL}
|
||||
api_key: ${LLM_API_KEY}
|
||||
provider: ${LLM_PROVIDER}
|
||||
```
|
||||
|
||||
启动:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
#### 启动后验证
|
||||
|
||||
```bash
|
||||
docker compose logs app | grep -E 'Built-in models? config'
|
||||
```
|
||||
|
||||
会看到类似:
|
||||
|
||||
```
|
||||
Built-in model upserted: id=builtin-openai-chat name=gpt-4o-mini type=KnowledgeQA
|
||||
Built-in model upserted: id=builtin-openai-embeddings name=text-embedding-3-small type=Embedding
|
||||
Built-in models config applied: 2 entries from /app/config/builtin_models.yaml.
|
||||
```
|
||||
|
||||
#### Docker 部署
|
||||
|
||||
在 `docker-compose.yml` 的 `app` 服务 `volumes` 块挂载文件:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
volumes:
|
||||
- ./config/builtin_models.yaml:/app/config/builtin_models.yaml:ro
|
||||
```
|
||||
|
||||
仓库提供了 `config/builtin_models.yaml.example` 作为起点,复制为 `config/builtin_models.yaml` 后按需修改。
|
||||
|
||||
### 方式二:直接 SQL 插入
|
||||
|
||||
支持的 provider:`generic`(自定义)、`openai`、`aliyun`、`zhipu`、`volcengine`、`hunyuan`、`deepseek`、`minimax`、`mimo`、`siliconflow`、`jina`、`openrouter`、`litellm`、`requesty`、`gemini`、`modelscope`、`moonshot`、`qianfan`、`qiniu`、`longcat`、`gpustack`
|
||||
|
||||
```sql
|
||||
-- 示例:LLM 内置模型
|
||||
INSERT INTO models (
|
||||
id, tenant_id, name, type, source, description,
|
||||
parameters, is_default, status, is_builtin
|
||||
) VALUES (
|
||||
'builtin-llm-001',
|
||||
10000,
|
||||
'gpt-4o-mini',
|
||||
'KnowledgeQA',
|
||||
'remote',
|
||||
'系统内置 LLM 模型',
|
||||
'{"base_url": "https://api.openai.com/v1", "api_key": "sk-xxx", "provider": "openai"}'::jsonb,
|
||||
false,
|
||||
'active',
|
||||
true
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- Embedding
|
||||
INSERT INTO models (
|
||||
id, tenant_id, name, type, source, description,
|
||||
parameters, is_default, status, is_builtin
|
||||
) VALUES (
|
||||
'builtin-embedding-001',
|
||||
10000,
|
||||
'text-embedding-3-small',
|
||||
'Embedding',
|
||||
'remote',
|
||||
'系统内置 Embedding 模型',
|
||||
'{"base_url": "https://api.openai.com/v1", "api_key": "sk-xxx", "provider": "openai", "embedding_parameters": {"dimension": 1536, "truncate_prompt_tokens": 0}}'::jsonb,
|
||||
false,
|
||||
'active',
|
||||
true
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- Rerank
|
||||
INSERT INTO models (
|
||||
id, tenant_id, name, type, source, description,
|
||||
parameters, is_default, status, is_builtin
|
||||
) VALUES (
|
||||
'builtin-rerank-001',
|
||||
10000,
|
||||
'bge-reranker-v2-m3',
|
||||
'Rerank',
|
||||
'remote',
|
||||
'系统内置 Rerank 模型',
|
||||
'{"base_url": "https://api.jina.ai/v1", "api_key": "jina-xxx", "provider": "jina"}'::jsonb,
|
||||
false,
|
||||
'active',
|
||||
true
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
```
|
||||
|
||||
### 验证插入结果
|
||||
|
||||
```sql
|
||||
SELECT id, name, type, is_builtin, status
|
||||
FROM models
|
||||
WHERE is_builtin = true
|
||||
ORDER BY type, created_at;
|
||||
```
|
||||
|
||||
## 将现有模型设置为内置模型
|
||||
|
||||
如果你已经手工创建了一个普通模型,想把它升级为内置模型:
|
||||
|
||||
```sql
|
||||
UPDATE models
|
||||
SET is_builtin = true
|
||||
WHERE id = '模型ID';
|
||||
```
|
||||
|
||||
## 移除内置模型
|
||||
|
||||
**从 YAML 删除条目即可。** 应用启动时会自动软删除 `models` 表中 YAML 不再声明的 YAML 托管行 —— 你不再需要手工跑 SQL。
|
||||
|
||||
工作原理:每条由 YAML 写入的行会被打上 `managed_by = 'yaml'` 标记。重启时 loader 走两步:
|
||||
|
||||
1. UPSERT 当前 YAML 中的所有条目(按 `id` 幂等,包含把之前软删过的 `deleted_at` 重置为 NULL —— 也就是说从 YAML 拿掉再加回来等于"复活")
|
||||
2. 软删除 `is_builtin = true AND managed_by = 'yaml' AND id NOT IN (当前 YAML 中的 id 集合)` 的行
|
||||
|
||||
**手工通过 SQL 插入的 builtin 行(`managed_by = ''`)永远不会被 loader 触碰**,与 YAML 完全隔离。
|
||||
|
||||
### 手工路径补充
|
||||
|
||||
如果你是走 SQL 路径管理的(`managed_by = ''`),删除仍然走老方法:
|
||||
|
||||
```sql
|
||||
-- 取消 builtin 标记,恢复为普通模型
|
||||
UPDATE models SET is_builtin = false WHERE id = '模型ID';
|
||||
|
||||
-- 或直接删除
|
||||
DELETE FROM models WHERE id = '模型ID';
|
||||
```
|
||||
|
||||
### 紧急关闭 YAML 接管
|
||||
|
||||
如果误改了 YAML 想立刻停用接管又不想清空文件,最快的方法是:把环境变量 `BUILTIN_MODELS_CONFIG` 指向一个不存在的路径并重启 —— loader 看到文件缺失会直接 no-op,**包括跳过 drift sweep**,已经写入的 YAML 托管行保留原状。
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **ID 命名规范**:建议使用 `builtin-{type}-{slug}` 的格式,例如 `builtin-openai-chat`、`builtin-rerank`
|
||||
2. **空间ID**:内置模型可以属于任意空间,默认 `10000`(与 `tenants_id_seq` 起点一致)
|
||||
3. **YAML 与 SQL 并存**:两种方式可以同时使用,loader 只动 `managed_by='yaml'` 的行;通过 SQL 插入的 builtin 行对 loader 完全不可见
|
||||
4. **`is_default` 单一保证**:YAML 中将某条 entry 标记 `is_default: true` 时,loader 会先把同 `(tenant_id, type)` 下的其它默认模型置为 `false`,避免 API 路径维护的"每类型一个默认模型"语义被破坏
|
||||
5. **重启即生效**:修改 YAML 后 `docker compose restart app` 即可让新配置生效
|
||||
6. **加密**:API Key 在 `parameters` JSONB 中以加密形式存储(若 `SYSTEM_AES_KEY` 已配置),未配置时降级为明文兼容路径
|
||||
7. **安全性**:前端会自动隐藏内置模型的 API Key 和 Base URL,但数据库中的原始数据仍然存在,请妥善保管数据库访问权限
|
||||
8. **解析错误自我保护**:YAML 解析失败时 loader 仅打 warning 并跳过 reconcile,**不会**执行 drift sweep,确保一个手抖的 YAML 改动不会大规模软删既有内置模型
|
||||
@@ -0,0 +1,170 @@
|
||||
# Chunking Guide
|
||||
|
||||
How WeKnora splits uploaded documents before embedding, why the defaults
|
||||
are what they are, and when to change them.
|
||||
|
||||
## Why chunking matters
|
||||
|
||||
Retrieval-Augmented Generation (RAG) works by embedding small slices of
|
||||
your documents into a vector index, then pulling the most relevant
|
||||
slices back at query time. The way a document is sliced — chunk size,
|
||||
overlap, where the cuts fall — directly drives retrieval recall and the
|
||||
quality of the answers your LLM produces.
|
||||
|
||||
Empirically (Vecta Feb-2026 benchmark across 50 academic papers):
|
||||
recursive splitting at ~512 tokens with ~15% overlap is the strongest
|
||||
single-knob baseline at 69% end-to-end accuracy, beating semantic
|
||||
chunking and over-engineered hybrids. WeKnora uses that as the
|
||||
foundation and layers smarter strategies on top when the document gives
|
||||
us structural cues.
|
||||
|
||||
## Adaptive 3-tier chunking
|
||||
|
||||
Set per knowledge base via the editor's **Chunking** sidebar (or the
|
||||
`strategy` field on the KB-config API).
|
||||
|
||||
| Strategy | When picked | What it does |
|
||||
|----------|-------------|--------------|
|
||||
| `auto` (recommended) | Default for new KBs | Profiles the document and picks the strongest tier from the chain below. |
|
||||
| `heading` | Markdown-style structure | Splits at `#` / `##` / `###` boundaries. Each chunk gets a breadcrumb context header (`# Top > ## Section`) prepended at embedding time. |
|
||||
| `heuristic` | PDF-style structure | Splits at form-feeds (page breaks), numbered sections, multilingual chapter markers (DE / EN / ZH), all-caps titles, and visual separators. |
|
||||
| `legacy` (= `recursive`) | Anything else, or as fallback | Pure recursive separator-based splitter — newest version with priority recursion and overlap-cap fixes. |
|
||||
|
||||
A document profiler runs first and counts structural signals (Markdown
|
||||
headings, form-feeds, chapter markers per language, all-caps lines,
|
||||
visual separators, blank-line bursts). Auto-strategy picks the tier
|
||||
chain based on those counts; a validator rejects obviously broken
|
||||
output (e.g. the heading splitter producing 200 single-line chunks)
|
||||
and falls through to the next tier.
|
||||
|
||||
## Settings reference
|
||||
|
||||
### Core
|
||||
|
||||
| Setting | Range | Default | Sweet spot for… |
|
||||
|---------|-------|---------|-----------------|
|
||||
| **Chunk size** | 100–4000 chars | 512 | Default works for most cases. 200–400 for FAQs / atomic Q&A. 1000–2000 for narrative documents. |
|
||||
| **Chunk overlap** | 0–500 chars | 80 (~15%) | 0 for FAQs and structured records. 80 (default) for general documents. 150–200 for argumentative texts where reasoning crosses chunks. |
|
||||
| **Separators** | string list | `["\n\n", "\n", "。", "!", "?", ";", ";"]` | Order matters — splitter tries higher-priority separators first and only falls back to lower ones when a piece is still oversize. |
|
||||
|
||||
### Parent-Child chunking
|
||||
|
||||
Two-level retrieval: **child chunks** (small, embedded for vector match)
|
||||
and **parent chunks** (larger, returned to the LLM for context).
|
||||
|
||||
| Setting | Range | Default | Notes |
|
||||
|---------|-------|---------|-------|
|
||||
| **Enable parent-child** | toggle | on | Recommended for documents > 10 pages. Skip for short FAQs to halve storage cost. |
|
||||
| **Parent chunk size** | 512–8192 chars | 4096 (~1000 EN tokens) | Larger for long-context LLMs (Claude, GPT-4-Turbo). Smaller (1024–2048) for local LLMs with 4k contexts. |
|
||||
| **Child chunk size** | 64–2048 chars | 384 (~95 EN tokens) | 128–256 for Q&A-style precise matching. 512–1024 if your embedder accepts >1000 tokens (E5 / BGE-large). |
|
||||
|
||||
### Advanced
|
||||
|
||||
| Setting | Range | Default | When to set |
|
||||
|---------|-------|---------|-------------|
|
||||
| **Token limit** | 0–8192 | 0 (off) | Activate when your embedding model has a small token cap. See table below. |
|
||||
| **Languages** | `de` / `en` / `zh` (multi-select) | empty (auto-detect) | Set explicitly for homogeneous corpora to narrow heuristic patterns. |
|
||||
|
||||
#### Token-limit guide per embedding model
|
||||
|
||||
| Embedder | Token limit | Recommended `tokenLimit` setting |
|
||||
|----------|-------------|----------------------------------|
|
||||
| OpenAI `text-embedding-3-small/large` | 8191 | **0 (leave off)** |
|
||||
| Anthropic Voyage-3 | 32000 | **0** |
|
||||
| Jina-embeddings-v3 | 8192 | **0** |
|
||||
| Cohere `embed-multilingual-v3` | 512 | **400** |
|
||||
| BGE-base / BGE-large / E5-large | 512 | **400** |
|
||||
| Sentence-Transformer `all-MiniLM-L6-v2` | 256 | **200** |
|
||||
|
||||
Rule of thumb: leave at 0 for any modern embedder with > 2000 tokens.
|
||||
Activate to 80% of the model's hard limit for smaller embedders so
|
||||
chunks always fit even for CJK content (which is denser per character).
|
||||
|
||||
## Use-case presets
|
||||
|
||||
| Workload | Strategy | ChunkSize | Overlap | Parent-Child |
|
||||
|----------|----------|-----------|---------|--------------|
|
||||
| FAQ / Q&A knowledge base | `auto` (likely picks legacy) | 200–400 | 0 | off |
|
||||
| Markdown documentation / wikis | `auto` (picks heading) | 512 | 80 | on |
|
||||
| PDF reports with page breaks | `auto` (picks heuristic) | 800–1200 | 100–150 | on |
|
||||
| Long-form narrative (books, articles) | `auto` (picks recursive) | 1000–2000 | 150–200 | on |
|
||||
| Code documentation | `legacy` | 800 | 100 | optional |
|
||||
| Mixed-language corpus | `auto`, languages = empty | 512 | 80 | on |
|
||||
| Tabular reports / CSV-derived | `legacy` | 400 | 0 | off |
|
||||
|
||||
## Debugging in the UI
|
||||
|
||||
The KB editor's **Chunking** sidebar has a "Test with sample text"
|
||||
collapsible at the bottom:
|
||||
|
||||
1. Paste a Markdown / plain-text snippet (max 64 KB).
|
||||
2. Click **Run preview**.
|
||||
3. The panel shows:
|
||||
- Selected strategy tier as a colored tag
|
||||
- Tiers that were rejected and why (e.g. "too many tiny chunks")
|
||||
- Document profile (heading counts, form-feeds, chapter markers,
|
||||
detected languages)
|
||||
- Size statistics over the full chunk set (avg / min / max / stddev)
|
||||
- Per-chunk cards with size in chars + approximate tokens, position
|
||||
range, the section breadcrumb (when set), and a content preview
|
||||
|
||||
This runs read-only against a goroutine-isolated splitter pass (5s
|
||||
timeout) — no DB writes, no embedding API calls. Use it to compare
|
||||
configurations against the same sample before triggering a re-upload.
|
||||
|
||||
## API
|
||||
|
||||
Three endpoints can write the chunking config. The KB-config update
|
||||
endpoint is the one wired to the editor UI and uses **camelCase** with a
|
||||
`documentSplitting` envelope:
|
||||
|
||||
```http
|
||||
PUT /api/v1/initialization/config/:kbId
|
||||
Authorization: Bearer <jwt>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"documentSplitting": {
|
||||
"chunkSize": 512,
|
||||
"chunkOverlap": 80,
|
||||
"separators": ["\n\n", "\n", "。", "!", "?", ";", ";"],
|
||||
"strategy": "auto",
|
||||
"tokenLimit": 0,
|
||||
"languages": ["de", "en"],
|
||||
"enableParentChild": true,
|
||||
"parentChunkSize": 4096,
|
||||
"childChunkSize": 384
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `strategy`, `tokenLimit`, and `languages` fields use pointer-based
|
||||
DTOs server-side: omitting them in the payload means "no change",
|
||||
sending an empty string / 0 / [] explicitly resets to default.
|
||||
|
||||
The KB CRUD endpoints (`POST /api/v1/knowledge-bases`,
|
||||
`PUT /api/v1/knowledge-bases/:id`) take the same fields but in
|
||||
**snake_case** under a `chunking_config` envelope:
|
||||
`{ "chunking_config": { "chunk_size": 512, "chunk_overlap": 80,
|
||||
"strategy": "auto", "token_limit": 0, "languages": ["de", "en"], ... } }`.
|
||||
|
||||
The preview endpoint at `POST /api/v1/chunker/preview` uses the snake_case
|
||||
form too and additionally takes a `text` field for the sample to chunk.
|
||||
|
||||
## Known trade-offs
|
||||
|
||||
- **Tier-1 heading-aware chunking** prepends the section breadcrumb to
|
||||
the embedding input, costing ~5% more tokens per chunk in exchange
|
||||
for ~30–50% fewer chunks on structured documents (net token savings
|
||||
on storage and at query time).
|
||||
- **Strategy switches do not auto-reindex** existing documents. After
|
||||
changing a KB's strategy, re-upload affected files (or trigger
|
||||
re-indexing via the UI) to apply the new chunking.
|
||||
- **OCR artifacts in PDFs** (vertical layout text broken character-
|
||||
by-character into separate lines) cannot be fixed by any splitter —
|
||||
this is a parser-side limitation. The heuristic tier still keeps
|
||||
chunks aligned to page boundaries, which mitigates the worst cases.
|
||||
- **The `recursive` strategy value** exists in the API for completeness
|
||||
but is intentionally hidden from the UI: it is functionally near
|
||||
`legacy` and adding a fifth dropdown option dilutes the meaningful
|
||||
choice between automatic / Markdown / heuristic / legacy.
|
||||
@@ -0,0 +1,28 @@
|
||||
# WeKnora 知识图谱
|
||||
|
||||
## 快速开始
|
||||
|
||||
- .env 配置相关环境变量
|
||||
- 启用 Neo4j: `NEO4J_ENABLE=true`
|
||||
- Neo4j URI: `NEO4J_URI=bolt://neo4j:7687`
|
||||
- Neo4j 用户名: `NEO4J_USERNAME=neo4j`
|
||||
- Neo4j 密码: `NEO4J_PASSWORD=password`
|
||||
|
||||
- 启动 Neo4j
|
||||
```bash
|
||||
docker-compose --profile neo4j up -d
|
||||
```
|
||||
|
||||
- 在知识库设置页面启用实体和关系提取,并根据提示配置相关内容
|
||||
|
||||
## 生成图谱
|
||||
|
||||
上传任意文档后,系统会自动提取实体和关系,并生成对应的知识图谱。
|
||||
|
||||

|
||||
|
||||
## 查看图谱
|
||||
|
||||
登录 `http://localhost:7474`,执行 `match (n) return (n)` 即可查看生成的知识图谱。
|
||||
|
||||
在对话时,系统会自动查询知识图谱,并获取相关知识。
|
||||
@@ -0,0 +1,16 @@
|
||||
# WeKnora Lite 与标准版区别
|
||||
|
||||
Lite 面向希望快速在本地使用、部署尽量简单的场景;标准版面向多空间协作与完整企业能力。主要差异如下。
|
||||
|
||||
|
||||
| 维度 | Lite | 标准版 |
|
||||
| --------- | ----------------------------------------------- | --------------------------------- |
|
||||
| **共享空间** | 不提供共享空间(成员邀请、跨成员共享知识库与智能体等) | 提供共享空间及空间隔离检索等协作能力 |
|
||||
| **空间与账号** | 单空间;开箱即用,**无需注册** | 多空间;通常需要注册、登录与组织管理 |
|
||||
| **文档解析** | 内置仅 **Simple** 类型解析引擎;可通过 **Cloud** 等方式接入其他解析能力 | 可配置多种解析引擎(含高精度等),与完整文档处理链路集成 |
|
||||
| **部署形态** | **单应用、零依赖**(不依赖独立的数据库、消息队列等外部服务栈) | 典型为 Docker Compose 等多服务部署,依赖与组件更多 |
|
||||
| **数据归属** | 数据**完全在本地**存储与处理 | 私有化部署时数据也可在本地;具体取决于你的部署方式 |
|
||||
| **网络暴露** | **默认仅本机访问**;可按需配置,**选择是否放行到公网** | 按部署与安全策略自行绑定地址与网关 |
|
||||
|
||||
|
||||
若你不需要多团队协作、复杂解析流水线与多服务架构,Lite 更适合个人或小团队在本机零依赖试用;需要共享空间、多空间与完整解析引擎矩阵时,请使用标准版。
|
||||
@@ -0,0 +1,312 @@
|
||||
# Langfuse 集成
|
||||
|
||||
WeKnora 内置了对 [Langfuse](https://langfuse.com) 的轻量级集成,用于统计 token 消耗、追踪 LLM 调用链路、并为每个对话生成可在 Langfuse 控制台查看的 trace。该集成解决 issue [#497](https://github.com/Tencent/WeKnora/issues/497)(token 使用量统计)和 discussion [#620](https://github.com/Tencent/WeKnora/discussions/620)(接入 Langfuse)。
|
||||
|
||||
## 1. 特性
|
||||
|
||||
- 自动上报 **chat / embedding / rerank / VLM(视觉语言模型)/ ASR(语音识别)** 全部 5 类模型调用的 prompt、响应和 token 使用量。
|
||||
- 为每个对话、检索、**文件上传及后续异步处理**创建一条端到端 **trace**。HTTP 请求是根,asynq 任务以 SPAN 的形式挂在同一条 trace 下,文档解析 → chunk embedding → 多模态 OCR/Caption → 摘要 / 问题生成全部在同一棵树里可见。
|
||||
- 支持 **流式响应**:记录首 token 延迟(Time-To-First-Token),完整响应在流结束后一次性写入。
|
||||
- **跨进程 trace 透传**:HTTP 层把 `trace_id` / `parent_observation_id` 注入 asynq payload,worker 在 asynq middleware 层自动 resume;定时任务(例如数据源同步)则退化为独立 trace,依然按任务类型(`asynq.<type>`)聚合。
|
||||
- **完全可选**:不配置 `LANGFUSE_*` 环境变量时,Langfuse 相关代码路径是 no-op,不产生任何性能开销。
|
||||
- **异步批量上报**:不阻塞业务请求;队列满时静默丢弃,观测数据不会影响用户对话。
|
||||
- **开箱即用的部署方式**:Docker Compose(`docker-compose.yml` 已内置环境变量)、Helm Chart(通过 `extraEnv`)、Lite 版本(本地单机)均支持。
|
||||
|
||||
## 2. 快速开始
|
||||
|
||||
### 2.1 获取 Langfuse 凭证
|
||||
|
||||
1. 登录 [cloud.langfuse.com](https://cloud.langfuse.com) 或自建 Langfuse 实例。
|
||||
2. 进入 `Project Settings → API Keys`,生成一对 `Public Key` / `Secret Key`。
|
||||
|
||||
### 2.2 按部署方式配置
|
||||
|
||||
#### (A)Docker Compose 部署(推荐)
|
||||
|
||||
`docker-compose.yml` 已经把所有 `LANGFUSE_*` 环境变量串到 `app` 服务。下面提供两种选择。
|
||||
|
||||
##### A-1) 接入 Langfuse Cloud(最简单)
|
||||
|
||||
只需要在 **`.env`** 里加 3 行:
|
||||
|
||||
```bash
|
||||
LANGFUSE_PUBLIC_KEY=pk-lf-xxxxxxxx
|
||||
LANGFUSE_SECRET_KEY=sk-lf-xxxxxxxx
|
||||
LANGFUSE_HOST=https://cloud.langfuse.com # 美区用 https://us.cloud.langfuse.com
|
||||
```
|
||||
|
||||
然后重启服务:
|
||||
|
||||
```bash
|
||||
docker compose up -d app
|
||||
docker compose logs -f app | grep Langfuse
|
||||
```
|
||||
|
||||
看到下面这行就说明已启用:
|
||||
|
||||
```
|
||||
[Langfuse] enabled host=https://cloud.langfuse.com flush_at=15 flush_interval=3s sample_rate=1.00
|
||||
```
|
||||
|
||||
##### A-2) 自建 Langfuse 栈(离线 / 内网 / 数据合规)
|
||||
|
||||
`docker-compose.yml` 内置了一个可选的 `langfuse` profile,用一条命令就能拉起 Langfuse v3。
|
||||
|
||||
**设计上已尽可能复用 WeKnora 已有容器,避免资源浪费**:
|
||||
|
||||
| 组件 | 来源 | 备注 |
|
||||
| --- | --- | --- |
|
||||
| PostgreSQL | 复用 `WeKnora-postgres` | 通过一次性的 `langfuse-db-init` 容器,在同一 pg 实例里创建独立的 `langfuse` 数据库。库级隔离,互不影响。 |
|
||||
| Redis | 复用 `WeKnora-redis` | 使用独立的 Redis DB 号(默认 DB 1,WeKnora 用 DB 0)。`REDIS_CONNECTION_STRING` 指定 DB 后缀。 |
|
||||
| ClickHouse | 新增 `langfuse-clickhouse` | Langfuse 专有(OLAP 事件存储),WeKnora 不用,必须独立。 |
|
||||
| MinIO | 新增 `langfuse-minio` | 故意和 WeKnora 的 `minio` 分开(后者是可选 profile,未必激活;Langfuse S3 要专属 bucket)。 |
|
||||
| Web / Worker | 新增 `langfuse-web` + `langfuse-worker` | Langfuse 应用本体。 |
|
||||
|
||||
最终 `--profile langfuse` 只新增 **4 个常驻容器 + 1 个一次性 init**,内存开销由原先的 ~1.5–2.5 GB 降到约 **1.0–1.5 GB**。
|
||||
|
||||
```bash
|
||||
# 1. 启动自建栈(ClickHouse 首次迁移大约需要 1-2 分钟)
|
||||
docker compose --profile langfuse up -d
|
||||
|
||||
# 2. 浏览器打开 http://localhost:3000 注册管理员账号
|
||||
# 然后在 Project Settings → API Keys 生成 Public/Secret Key
|
||||
|
||||
# 3. 把 key 填回 .env 并把 HOST 改成容器内部地址
|
||||
cat >> .env <<'EOF'
|
||||
LANGFUSE_HOST=http://langfuse-web:3000
|
||||
LANGFUSE_PUBLIC_KEY=pk-lf-xxxxxxxx
|
||||
LANGFUSE_SECRET_KEY=sk-lf-xxxxxxxx
|
||||
EOF
|
||||
|
||||
# 4. 让 app 重新加载配置
|
||||
docker compose up -d app
|
||||
```
|
||||
|
||||
> ⚠️ **生产部署安全提示**:`.env.example` 里的默认密码 / `SALT` / `ENCRYPTION_KEY` 都是开发占位符,生产环境必须用以下命令重新生成:
|
||||
>
|
||||
> ```bash
|
||||
> echo "LANGFUSE_SALT=$(openssl rand -base64 32)"
|
||||
> echo "LANGFUSE_ENCRYPTION_KEY=$(openssl rand -hex 32)"
|
||||
> echo "LANGFUSE_NEXTAUTH_SECRET=$(openssl rand -base64 32)"
|
||||
> ```
|
||||
>
|
||||
> 同时把 `LANGFUSE_DB_PASSWORD` / `LANGFUSE_CLICKHOUSE_PASSWORD` / `LANGFUSE_REDIS_PASSWORD` / `LANGFUSE_MINIO_PASSWORD` 全部换成强密码。完整变量清单见 `.env.example` 的 "Langfuse 自建栈配置" 段。
|
||||
|
||||
##### 通用调优
|
||||
|
||||
可选调优变量(`LANGFUSE_FLUSH_AT`、`LANGFUSE_SAMPLE_RATE` 等)都已经在 `docker-compose.yml` 中预设直通,只要在 `.env` 追加对应行即可生效。完整列表见 `.env.example` 的 Langfuse 段,或本文第 3 节。
|
||||
|
||||
##### 资源开销估算(A-2 自建方案)
|
||||
|
||||
| 组件 | 类型 | 典型 RSS | 备注 |
|
||||
| --- | --- | --- | --- |
|
||||
| langfuse-db-init | 一次性 | – | 创建 `langfuse` 数据库后立即退出 |
|
||||
| langfuse-web | 常驻 | 300–500 MB | Next.js |
|
||||
| langfuse-worker | 常驻 | 200–400 MB | Node.js,Queue consumer |
|
||||
| langfuse-clickhouse | 常驻 | 500 MB–1 GB | 首次迁移稍高,稳态约 500 MB |
|
||||
| langfuse-minio | 常驻 | 100–200 MB | |
|
||||
| (复用)WeKnora-postgres | – | +~50 MB | 多一个 `langfuse` 数据库 |
|
||||
| (复用)WeKnora-redis | – | +30–80 MB | 共用实例的 DB 1 |
|
||||
| **新增合计** | | **≈ 1.0–1.5 GB** | 推荐 3 GB+ 可用内存 |
|
||||
|
||||
> 和"完全隔离各建一套 pg/redis"方案相比,这里节省了约 **400–500 MB** 内存。代价是 WeKnora 的 pg/redis 容量规划需要为 Langfuse 预留一点余量;Langfuse 写入量并不大(只是元数据 + 任务队列,事件主体走 ClickHouse),实际影响很小。
|
||||
|
||||
对单机部署而言,若只想使用 Langfuse Cloud 方案(A-1),**完全不需要**这些容器;原有服务 CPU/内存占用不变。
|
||||
|
||||
##### 生产环境下的注意事项
|
||||
|
||||
- **WeKnora-redis 的驱逐策略**:Langfuse 建议 `maxmemory-policy noeviction`(避免 Redis 在内存紧张时丢弃队列任务)。如果 WeKnora 的 redis 未配置该策略,建议在 `docker-compose.yml` 的 redis command 中加上 `--maxmemory-policy noeviction`。
|
||||
- **备份**:`pg_dump -d langfuse` 可独立备份 Langfuse 的元数据;事件数据在 ClickHouse 卷(`langfuse_clickhouse_data`)中。
|
||||
- **想彻底隔离**(跨机部署、强运维隔离):可以直接把 `langfuse-web` / `langfuse-worker` 的 `DATABASE_URL` 和 `REDIS_CONNECTION_STRING` 指向任意外部 pg/redis(例如 RDS + ElastiCache);`langfuse-db-init` 容器可以选择不启动,手动在目标 pg 上 `CREATE DATABASE langfuse` 即可。
|
||||
|
||||
#### (B)WeKnora Lite(单机)
|
||||
|
||||
在 `.env.lite`(或启动脚本导出的环境变量)里加:
|
||||
|
||||
```bash
|
||||
LANGFUSE_PUBLIC_KEY=pk-lf-xxxxxxxx
|
||||
LANGFUSE_SECRET_KEY=sk-lf-xxxxxxxx
|
||||
LANGFUSE_HOST=https://cloud.langfuse.com
|
||||
```
|
||||
|
||||
启动 `weknora-lite`(或 macOS `.app`)后效果同上。
|
||||
|
||||
#### (C)Helm Chart 部署
|
||||
|
||||
在 `values.yaml` 的 `app.extraEnv` 添加:
|
||||
|
||||
```yaml
|
||||
app:
|
||||
extraEnv:
|
||||
- name: LANGFUSE_PUBLIC_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: langfuse-credentials
|
||||
key: public_key
|
||||
- name: LANGFUSE_SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: langfuse-credentials
|
||||
key: secret_key
|
||||
- name: LANGFUSE_HOST
|
||||
value: https://cloud.langfuse.com
|
||||
```
|
||||
|
||||
建议把 Secret Key 放到 Kubernetes Secret 中,切勿写进 values.yaml。
|
||||
|
||||
#### (D)二进制 / 源码运行
|
||||
|
||||
```bash
|
||||
export LANGFUSE_PUBLIC_KEY="pk-lf-xxxx"
|
||||
export LANGFUSE_SECRET_KEY="sk-lf-xxxx"
|
||||
export LANGFUSE_HOST="https://cloud.langfuse.com"
|
||||
./weknora-server
|
||||
```
|
||||
|
||||
#### (E)本地开发(`docker-compose.dev.yml` + `go run`)
|
||||
|
||||
`docker-compose.dev.yml` 只启动基础设施容器(postgres/redis/docreader 等),`app` 走本地 `go run ./cmd/server`。Langfuse 的两种接入方式:
|
||||
|
||||
**E-1) 直连 Langfuse Cloud(dev 最常见)**
|
||||
|
||||
无需改任何 compose 文件,本地 shell 导出即可:
|
||||
|
||||
```bash
|
||||
export LANGFUSE_PUBLIC_KEY="pk-lf-xxxx"
|
||||
export LANGFUSE_SECRET_KEY="sk-lf-xxxx"
|
||||
export LANGFUSE_HOST="https://cloud.langfuse.com"
|
||||
go run ./cmd/server
|
||||
```
|
||||
|
||||
**E-2) 本地自建栈调试**
|
||||
|
||||
dev compose 也支持对称的 `langfuse` profile(复用同一个 dev postgres + redis):
|
||||
|
||||
```bash
|
||||
# 拉起基础设施 + Langfuse 栈
|
||||
docker compose -f docker-compose.dev.yml up -d postgres redis docreader
|
||||
docker compose -f docker-compose.dev.yml --profile langfuse up -d
|
||||
|
||||
# 浏览器打开 http://localhost:3000 注册并生成 key
|
||||
|
||||
# 本地 app 接入(注意是 localhost,不是 langfuse-web,因为 go run 跑在宿主机)
|
||||
export LANGFUSE_HOST=http://localhost:3000
|
||||
export LANGFUSE_PUBLIC_KEY=pk-lf-xxxxxxxx
|
||||
export LANGFUSE_SECRET_KEY=sk-lf-xxxxxxxx
|
||||
go run ./cmd/server
|
||||
```
|
||||
|
||||
Dev 相关容器都带 `-dev` 后缀、用独立网络 `WeKnora-network-dev`,和生产 compose **不冲突**。
|
||||
|
||||
### 2.3 验证
|
||||
|
||||
发起一次知识问答(`POST /api/v1/knowledge-chat/:session_id`)或知识检索(`POST /api/v1/knowledge-search`)。等待 3 秒(或批量大小达到 `flush_at`)后,Langfuse 控制台的 **Traces** 页面会出现对应的 trace:
|
||||
|
||||
- 顶层节点:HTTP 请求(带 `userId` / `sessionId`)。
|
||||
- 子节点依次为 rerank、chat、VLM 等具体模型调用,点击可查看 prompt、响应以及 usage(prompt/completion/total tokens)。
|
||||
- 流式对话会额外标注 Time-To-First-Token。
|
||||
|
||||
## 3. 环境变量参考
|
||||
|
||||
| 变量名 | 默认值 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `LANGFUSE_ENABLED` | 自动 | 显式开关。未设置时,只要 `PUBLIC_KEY` + `SECRET_KEY` 都存在就自动启用。支持 `true/false/1/0/yes/no`。 |
|
||||
| `LANGFUSE_HOST` | `https://cloud.langfuse.com` | Langfuse 实例地址。美区用 `https://us.cloud.langfuse.com`,自建实例填 `https://langfuse.your-domain.com`。 |
|
||||
| `LANGFUSE_PUBLIC_KEY` | — | 项目 Public Key(`pk-lf-...`)。 |
|
||||
| `LANGFUSE_SECRET_KEY` | — | 项目 Secret Key(`sk-lf-...`),请走密钥管理工具注入,不要提交到仓库。 |
|
||||
| `LANGFUSE_RELEASE` | — | 可选,上报到 Langfuse 的版本号,例如 CI 构建号。 |
|
||||
| `LANGFUSE_ENVIRONMENT` | — | 可选,环境标签(`production` / `staging` / `dev`),方便在 UI 过滤。 |
|
||||
| `LANGFUSE_FLUSH_AT` | `15` | 批处理大小:缓冲区积累到该数量立即上报。 |
|
||||
| `LANGFUSE_FLUSH_INTERVAL` | `3s` | 定时刷新间隔。支持 `500ms`、`5s`、`1m` 等 Go duration 写法;纯数字按秒处理。 |
|
||||
| `LANGFUSE_QUEUE_SIZE` | `2048` | 内存队列容量。队列满时新事件会被静默丢弃(避免拖慢业务)。 |
|
||||
| `LANGFUSE_REQUEST_TIMEOUT` | `10s` | 单次 HTTP ingest 请求超时。 |
|
||||
| `LANGFUSE_SAMPLE_RATE` | `1.0` | 采样率 (0..1)。`0` 视为 `1.0`。高流量环境可下调。 |
|
||||
| `LANGFUSE_DEBUG` | `false` | 打开后会在 WeKnora 日志里打印上报失败的详细原因,排障期间临时开启。 |
|
||||
|
||||
## 4. 观测数据说明
|
||||
|
||||
| Langfuse 概念 | WeKnora 对应 | 备注 |
|
||||
| --- | --- | --- |
|
||||
| Trace | 一次 HTTP 请求(含其触发的所有 asynq 任务) | 对于 `knowledge-chat`、`agent-chat`、`knowledge-search`、`generate_title`、`evaluation`、模型连通性测试等在线请求;以及文件上传/URL 入库/manual/reparse/move/copy、FAQ 导入、知识修改、wiki auto-fix、数据源手工触发等入库请求,HTTP 层都会开启 trace,并把 `trace_id` / `parent_observation_id` 注入 asynq payload。 |
|
||||
| Span(type=SPAN) | 每个 asynq 任务的执行窗口 / 每次 Agent 执行及其每一轮 / 每次工具调用 | 由 `internal/tracing/langfuse/AsynqMiddleware` 在 `mux.Use` 注册;对每个 handler 自动创建 `asynq.<task_type>` 的 SPAN,并记录 `task_id` / `queue` / `retry` / `payload_bytes`。定时任务(无上游 trace)会退化为 `asynq.<task_type>` 独立 trace。**Agent 相关**:`AgentEngine.Execute` 会开 `agent.execute` 顶层 SPAN,其下每一轮 ReAct 循环开 `agent.round.N` SPAN,每次工具调用开 `agent.tool.<tool_name>` SPAN(参数、输出、耗时、成败、错误都会写入)。 |
|
||||
| Generation(type=GENERATION) | 每次 chat / embedding / rerank / VLM / ASR 调用 | 若位于 span 下会自动设置 `parentObservationId`,所以 Langfuse UI 呈现 trace → asynq-span → generation 的树状结构;Agent 模式下是 trace → agent.execute → agent.round.N → (chat.completion.stream + agent.tool.X → rerank/embedding...) 的完整树。 |
|
||||
| Input Tokens | `TokenUsage.PromptTokens` | 来自模型返回的 usage 字段。 |
|
||||
| Output Tokens | `TokenUsage.CompletionTokens` | 来自模型返回的 usage 字段。 |
|
||||
| Total Tokens | `TokenUsage.TotalTokens` | 大多数厂商返回;未返回时自动求和。 |
|
||||
| Cache Read Tokens | `TokenUsage.CacheReadTokens` | 从 OpenAI-compatible `cached_tokens`、DeepSeek 原生 hit tokens 或 Anthropic cache read tokens 归一化。 |
|
||||
| Cache Write Tokens | `TokenUsage.CacheWriteTokens` | Anthropic cache creation 或供应商等价字段;不与 Input Tokens 重复相加。 |
|
||||
| Cache Miss Tokens | `TokenUsage.CacheMissTokens` | 已上报缓存口径下未从缓存读取的输入 token。 |
|
||||
| Generation Metadata | `call_purpose` / `prompt_prefix_fingerprint` | 用调用用途分组缓存指标;前缀只上报不可逆短哈希,不上报原始 prompt。 |
|
||||
| `userId` | `X-User-ID` / 空间 ID | 未登录时退化为 `tenant:<id>`,方便按空间汇总消耗;enqueue 时会写入 payload,worker 在无上游 trace 的场景也能保留归属。 |
|
||||
| `sessionId` | URL 中的 `:session_id`(或 `RequestID` 兜底) | 可以在 Langfuse 的 Sessions 视图聚合一整场对话,或按单次异步批次聚合。 |
|
||||
| Time-To-First-Token | 流式调用首条有效 chunk 的时间 | 通过 `generation-update.completionStartTime` 上报。 |
|
||||
|
||||
### 覆盖到的 asynq 任务类型
|
||||
|
||||
下表列出当前会在 Langfuse 里自动出现对应 SPAN 的 asynq 任务;每种任务的 payload 均已嵌入 `types.TracingContext`,enqueue 时由 `langfuse.InjectTracing(ctx, &payload)` 从当前 HTTP trace 拷出 `trace_id` / `parent_observation_id`。
|
||||
|
||||
| 任务类型常量 | Handler | 典型触发来源 |
|
||||
| --- | --- | --- |
|
||||
| `document:process` | `knowledgeService.ProcessDocument` | 文件 / URL / 文本 / file_url 四种入库;reparse;知识库克隆内部重派发 |
|
||||
| `manual:process` | `knowledgeService.ProcessManualKnowledge` | 手工知识新建 / 更新 |
|
||||
| `image:multimodal` | `ImageMultimodalService.Handle` | 文档解析时发现图片 |
|
||||
| `knowledge:post_process` | `KnowledgePostProcessService.Handle` | 文档解析完成后统一调度 summary/question |
|
||||
| `summary:generation` / `question:generation` | `KnowledgePostProcessService` 子任务 | 由 `knowledge:post_process` 派发 |
|
||||
| `chunk:extract` | `ChunkExtractor.Handle` | 图谱提取(NEO4J 启用时) |
|
||||
| `datatable:summary` | `DataTableSummaryService.Handle` | 表格文件解析 |
|
||||
| `faq:import` | FAQ 批量导入 handler | FAQ 导入 / 批量创建 |
|
||||
| `knowledge:move` / `knowledge:list_delete` / `index:delete` / `kb:clone` / `kb:delete` | 知识移动 / 批量删除 / 索引清理 / 知识库复制 / 知识库删除 | 对应 HTTP 路由 |
|
||||
| `wiki:ingest` | `wikiIngestService.ProcessWikiIngest` | Wiki auto-fix / 重建链接 |
|
||||
| `datasource:sync` | `dataSourceSyncService.Handle` | 数据源手动触发 + 定时调度(定时场景下 trace 为 standalone) |
|
||||
|
||||
### 各模型的 usage 处理策略
|
||||
|
||||
| 模型类型 | 上报名称 | Token 计量方式 | 备注 |
|
||||
| --- | --- | --- | --- |
|
||||
| Chat | `chat.completion` / `chat.completion.stream` | 直接使用模型返回的 `prompt_tokens` / `completion_tokens` / `total_tokens` | 流式请求会记录 TTFT。 |
|
||||
| Embedding | `embedding.embed` / `embedding.batch_embed` | 模型未返回 usage 时按 `rune_count/4 + 1` 估算 input tokens | 批量接口会上报批量大小和前 5 条文本预览,避免把整批内容塞进 trace。 |
|
||||
| Rerank | `rerank` | 按 `query + 所有文档` 的 rune 数估算 input tokens | 输出只上报前 10 条 `(index, score)`。 |
|
||||
| VLM | `vlm.predict` | prompt/result 分别按 `rune/4` 估算 input/output | 不上传原始图片字节;仅记录图片数量与总字节大小。 |
|
||||
| ASR | `asr.transcribe` | 以 **秒**(`SECONDS`)为计量单位,取转录结果最后一个 segment 的 `end` 作为音频时长 | 便于 Langfuse 按"分钟"结算 Whisper 类 API。 |
|
||||
|
||||
> Tip:Langfuse 的 `Settings → Models` 页面可以为自定义模型(本地 Ollama、阿里云百炼等)配置单价(每 1K tokens、每分钟等),Langfuse 会据此自动核算费用。
|
||||
|
||||
## 5. 高流量部署建议
|
||||
|
||||
- **调高 `LANGFUSE_FLUSH_AT`** 到 50–100,降低 ingest HTTP 调用频率。
|
||||
- **采样**:把 `LANGFUSE_SAMPLE_RATE=0.1` 只采样 10% 的对话,生产成本与信噪比通常能得到较好的平衡。
|
||||
- **扩大 `LANGFUSE_QUEUE_SIZE`** 至 8192,防止短时峰值触发事件丢弃。
|
||||
- 将 Langfuse 实例部署在离 WeKnora 同机房(例如自建 Langfuse + 内网地址),可以显著降低上报延迟。
|
||||
- 打开 `LANGFUSE_DEBUG=true` 几分钟即可确认链路,生产环境常态下关闭,避免日志噪音。
|
||||
|
||||
## 6. 禁用
|
||||
|
||||
删除或留空 `LANGFUSE_PUBLIC_KEY` / `LANGFUSE_SECRET_KEY`,或显式设置 `LANGFUSE_ENABLED=false`,再重启服务即可。所有 Langfuse 相关代码路径会回退到 no-op,不会影响其他观测组件(OpenTelemetry、LLM Debug Log)。
|
||||
|
||||
## 7. 故障排查
|
||||
|
||||
| 现象 | 建议排查步骤 |
|
||||
| --- | --- |
|
||||
| 启动日志没有 `[Langfuse] enabled` | 检查 `LANGFUSE_PUBLIC_KEY` / `LANGFUSE_SECRET_KEY` 是否被服务进程读到;容器里可 `env \| grep LANGFUSE` 验证。 |
|
||||
| 控制台看不到 trace | 打开 `LANGFUSE_DEBUG=true`,观察日志中是否有 `[Langfuse] flush ... failed`。常见原因:`LANGFUSE_HOST` 错误、企业防火墙拦截 HTTPS、Secret Key 轮换后未更新。 |
|
||||
| 部分 chunk 缺失 | 调大 `LANGFUSE_QUEUE_SIZE`;确认 Langfuse ingest API 没有返回 429/503。 |
|
||||
| token 数为 0 | 该模型在返回中未提供 usage(常见于部分本地 Ollama / 自建模型)。可在模型侧开启 usage 统计,或在 Langfuse 配置里为该模型提供 tokenizer。 |
|
||||
|
||||
## 8. 代码位置
|
||||
|
||||
- `internal/tracing/langfuse/` — Langfuse 客户端、异步批量上报、Gin 中间件、**asynq middleware**、Span / Trace resume 实现。
|
||||
- `tracer.go` — 暴露 `Trace` / `Span` / `Generation` + `StartTrace` / `StartSpan` / `StartGeneration` / `ResumeTrace`。
|
||||
- `asynq.go` — `AsynqMiddleware()` 统一在 mux 上包 handler;`InjectTracing(ctx, payload)` 在 enqueue 侧把 trace/span ID 注入 payload。
|
||||
- `middleware.go` — Gin 中间件 + `shouldTrace` 白名单(覆盖 chat / 入库 / FAQ / wiki / 数据源等路径)。
|
||||
- `internal/types/tracing.go` — `TracingContext` POCO,所有 asynq payload 通过嵌入此结构携带 `lf_trace_id` / `lf_parent_obs_id` / `lf_user_id` / `lf_session_id`。
|
||||
- `internal/models/chat/langfuse_wrapper.go` — Chat 调用装饰器(含流式)。
|
||||
- `internal/models/embedding/langfuse_wrapper.go` — Embedding 调用装饰器。
|
||||
- `internal/models/rerank/langfuse_wrapper.go` — Rerank 调用装饰器。
|
||||
- `internal/models/vlm/langfuse_wrapper.go` — VLM(视觉语言模型)调用装饰器。
|
||||
- `internal/models/asr/langfuse_wrapper.go` — ASR(语音识别)调用装饰器。
|
||||
- `internal/agent/engine.go` — `agent.execute` 顶层 SPAN 和 `agent.round.<N>` 每轮 SPAN。
|
||||
- `internal/agent/act.go` — `agent.tool.<tool_name>` 工具调用 SPAN(包含参数、输出、耗时、成败)。
|
||||
- `internal/router/router.go` — 注册 `langfuse.GinMiddleware()`。
|
||||
- `internal/router/task.go` — 在 asynq mux 上 `mux.Use(langfuse.AsynqMiddleware())`,使所有 handler 自动被 trace。
|
||||
- `internal/container/container.go` — 初始化 + 资源清理。
|
||||
- `docker-compose.yml` / `.env.example` / `.env.lite.example` — 预置 `LANGFUSE_*` 环境变量直通。
|
||||
@@ -0,0 +1,30 @@
|
||||
## MCP 功能使用说明
|
||||
|
||||
### 功能概述
|
||||
- MCP(Model Context Protocol)让 WeKnora 可以安全地连接外部工具或数据源,扩展 Agent 在推理时可调用的能力。
|
||||
- 在前端 `设置 > MCP 服务`(`frontend/src/views/settings/McpSettings.vue`)中集中管理所有服务,无需手动改配置文件。
|
||||
- 每个服务都包含名称、传输方式(SSE / HTTP Streamable / Stdio)、连接地址或命令、认证信息以及高级超时与重试策略。
|
||||
|
||||
### 入口与界面
|
||||
- 打开控制台左侧菜单 `设置 -> MCP 服务`,即可看到当前空间下的所有 MCP 服务列表。
|
||||
- 列表中可快速启停服务、查看描述,并通过右侧菜单执行“测试 / 编辑 / 删除”。
|
||||
- “添加服务”按钮会弹出 `McpServiceDialog`,用于创建或修改服务。
|
||||
|
||||
### 常用操作流程
|
||||
1. **新建服务**
|
||||
- 点击“添加服务”,填写名称与描述,选择传输方式。
|
||||
- SSE / HTTP Streamable 需提供可访问的服务 URL;Stdio 需配置 `uvx`/`npx` 命令与参数,可附加环境变量。
|
||||
- 根据需要填写 API Key、Bearer Token、超时与重试策略,保存后服务会出现在列表中。
|
||||
2. **启停服务**
|
||||
- 在列表开关中切换启用状态,系统会即时调用后端 `updateMCPService`,失败时会自动回滚状态并弹出提示。
|
||||
3. **连接测试**
|
||||
- 通过更多菜单选择“测试”,前端会调用 `/api/v1/mcp-services/{id}/test` 并弹出 `McpTestResult`。
|
||||
- 成功时会展示服务可用的工具清单(含输入 schema)和资源列表;失败时会显示错误信息,方便排查网络或鉴权问题。
|
||||
4. **编辑 / 删除**
|
||||
- “编辑”会带出原有配置,修改后保存即可。
|
||||
- “删除”需要在弹窗中确认,完成后列表自动刷新。
|
||||
|
||||
### 使用建议
|
||||
- **传输方式选择**:优先使用 SSE 获取流式体验;需要标准 HTTP Streamable 兼容时再切换;本地调试或离线环境适合使用 Stdio 并在同机启动 MCP Server。
|
||||
- **鉴权管理**:将 API Key / Token 保存在“认证配置”中,生产环境建议单独创建最小权限 Key,并定期轮换。
|
||||
- **重试策略**:对公网或第三方服务适当提高 `retry_count` 与 `retry_delay`,避免间歇性超时导致 Agent 中断
|
||||
@@ -0,0 +1,646 @@
|
||||
# WeKnora OIDC 认证调用流程
|
||||
|
||||
本文档说明 WeKnora 当前 OIDC 登录能力的实际调用过程,覆盖:
|
||||
|
||||
- 前端如何判断是否展示 OIDC 登录入口
|
||||
- 用户点击 OIDC 登录后的前后端调用链路
|
||||
- 后端如何与 OIDC Provider 交互
|
||||
- 登录成功后前端如何接收结果并落盘本地登录态
|
||||
- 关键配置项与本地联调方式
|
||||
|
||||
本文内容基于当前项目实现,相关代码主要位于:
|
||||
|
||||
- 后端路由:`internal/router/router.go`
|
||||
- 认证处理:`internal/handler/auth.go`
|
||||
- 认证服务:`internal/application/service/user.go`
|
||||
- 配置定义:`internal/config/config.go`
|
||||
- 前端登录页:`frontend/src/views/auth/Login.vue`
|
||||
- 前端全局回调处理:`frontend/src/App.vue`
|
||||
- 前端认证 API:`frontend/src/api/auth/index.ts`
|
||||
- 本地 Dex 示例:`misc/dex-config.yaml`
|
||||
|
||||
---
|
||||
|
||||
## 1. 整体设计说明
|
||||
|
||||
本项目的 OIDC 登录采用的是 **后端发起授权参数生成、后端接收回调并完成 code 换 token、前端通过 URL hash 接收最终登录结果** 的模式。
|
||||
|
||||
和常见的纯前端 OIDC SDK 不同,WeKnora 的特点是:
|
||||
|
||||
1. **前端只负责发起跳转**,不直接和 OIDC Provider 交换 token。
|
||||
2. **后端负责用授权码 `code` 向 OIDC Provider 换取 token**。
|
||||
3. 后端拿到 OIDC 用户信息后,会:
|
||||
- 查找本地用户;
|
||||
- 若用户不存在,则自动创建本地账号和默认空间;
|
||||
- 最终签发 WeKnora 自己的本地 JWT(`token` / `refresh_token`)。
|
||||
4. 后端不会直接把登录结果放在 query string 中,而是:
|
||||
- 将登录结果 JSON( `success`、`token`、`refresh_token` 等字段)序列化后再做 base64url 编码;
|
||||
- 以 `#oidc_result=...` 的形式重定向回前端;
|
||||
- 前端在 `App.vue` 中统一解析 hash,再调用 `/api/v1/auth/me` 补全用户和空间信息。
|
||||
|
||||
因此,**OIDC Provider 的 token 只用于后端换取用户身份,本项目真正的业务访问凭证仍然是 WeKnora 自己签发的 JWT**。
|
||||
|
||||
---
|
||||
|
||||
## 2. 相关接口
|
||||
|
||||
当前 OIDC 相关接口均注册在 `internal/router/router.go` 中:
|
||||
|
||||
- `GET /api/v1/auth/oidc/config`
|
||||
- 获取 OIDC 是否启用,以及 Provider 展示名称
|
||||
- `GET /api/v1/auth/oidc/url`
|
||||
- 生成第三方登录跳转地址
|
||||
- `GET /api/v1/auth/oidc/callback`
|
||||
- OIDC Provider 回调地址
|
||||
|
||||
其中,前端常规登录仍然使用:
|
||||
|
||||
- `POST /api/v1/auth/login`
|
||||
- `POST /api/v1/auth/refresh`
|
||||
- `POST /api/v1/auth/logout`
|
||||
- `GET /api/v1/auth/me`
|
||||
|
||||
---
|
||||
|
||||
## 3. 调用流程图
|
||||
|
||||
### 3.1 总体时序图
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant U as 用户浏览器
|
||||
participant FE as 前端(Login/App)
|
||||
participant BE as WeKnora 后端
|
||||
participant OP as OIDC Provider
|
||||
|
||||
FE->>BE: GET /api/v1/auth/oidc/config
|
||||
BE-->>FE: { enabled, provider_display_name }
|
||||
|
||||
U->>FE: 点击“OIDC 登录”
|
||||
FE->>BE: GET /api/v1/auth/oidc/url?redirect_uri=...
|
||||
BE-->>FE: { success, authorization_url, state }
|
||||
FE->>OP: 浏览器跳转到 authorization_url
|
||||
|
||||
OP-->>BE: GET /api/v1/auth/oidc/callback?code=...&state=...
|
||||
BE->>OP: POST token endpoint (code 换 token)
|
||||
OP-->>BE: access_token / id_token
|
||||
BE->>OP: GET userinfo endpoint(可选)
|
||||
OP-->>BE: 用户信息 claims
|
||||
|
||||
BE->>BE: 查找/自动创建本地用户
|
||||
BE->>BE: 签发本地 token、refresh_token
|
||||
BE-->>FE: 302 到 /#oidc_result=...
|
||||
|
||||
FE->>FE: App.vue 解析 hash
|
||||
FE->>BE: GET /api/v1/auth/me
|
||||
BE-->>FE: { user, tenant }
|
||||
FE->>FE: 写入 authStore/token/user/tenant
|
||||
FE-->>U: 跳转 /platform/knowledge-bases
|
||||
```
|
||||
|
||||
### 3.2 后端回调处理分支图
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[OIDC Provider 回调到 /api/v1/auth/oidc/callback] --> B{query 中是否有 error}
|
||||
B -- 是 --> C[拼接 #oidc_error 和 error_description]
|
||||
C --> D[302 重定向到前端登录页]
|
||||
|
||||
B -- 否 --> E[解析 state]
|
||||
E --> F{state 是否合法且包含 redirect_uri}
|
||||
F -- 否 --> G[302 到前端并带 #oidc_error=invalid_state]
|
||||
|
||||
F -- 是 --> H{是否有 code}
|
||||
H -- 否 --> I[302 到前端并带 #oidc_error=missing_code]
|
||||
|
||||
H -- 是 --> J[调用 LoginWithOIDC,传入 code 和 redirect_uri]
|
||||
J --> K[向 OIDC token endpoint 换 token]
|
||||
K --> L[解析 id_token / 调用 userinfo]
|
||||
L --> M{本地用户是否存在}
|
||||
M -- 否 --> N[自动注册新用户并创建默认空间]
|
||||
M -- 是 --> O[使用现有用户]
|
||||
N --> P[签发 WeKnora JWT]
|
||||
O --> P
|
||||
P --> Q[编码最小必要登录结果为 oidc_result]
|
||||
Q --> R[302 重定向到前端首页 hash]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 前端调用流程
|
||||
|
||||
## 4.1 登录页初始化:决定是否展示 OIDC 登录按钮
|
||||
|
||||
登录页组件位于 `frontend/src/views/auth/Login.vue`。
|
||||
|
||||
页面加载时会执行:
|
||||
|
||||
```ts
|
||||
loadOIDCConfig()
|
||||
```
|
||||
|
||||
该函数调用:
|
||||
|
||||
```ts
|
||||
getOIDCConfig() -> GET /api/v1/auth/oidc/config
|
||||
```
|
||||
|
||||
后端 `GetOIDCConfig` 会读取 `configInfo.OIDCAuth`:
|
||||
|
||||
- `enabled`: 是否启用 OIDC
|
||||
- `provider_display_name`: 登录按钮上展示的供应商名称
|
||||
|
||||
前端据此决定:
|
||||
|
||||
- 是否显示 OIDC 登录按钮;
|
||||
- 按钮文案是否显示为“使用 XXX 登录”。
|
||||
|
||||
---
|
||||
|
||||
## 4.2 用户点击 OIDC 登录按钮
|
||||
|
||||
用户点击按钮后,`Login.vue` 中会执行 `handleOIDCLogin()`。
|
||||
|
||||
核心逻辑:
|
||||
|
||||
1. 前端先构造后端回调地址:
|
||||
|
||||
```ts
|
||||
const getBackendOIDCRedirectURI = () => `${window.location.origin}/api/v1/auth/oidc/callback`
|
||||
```
|
||||
|
||||
其中:
|
||||
|
||||
- `redirect_uri`:提供给 OIDC Provider 的回调地址,必须是后端地址。
|
||||
|
||||
登录成功后,后端固定回跳前端首页 `/`,并通过 hash 传递 OIDC 结果。
|
||||
|
||||
2. 前端调用:
|
||||
|
||||
```ts
|
||||
GET /api/v1/auth/oidc/url?redirect_uri=...
|
||||
```
|
||||
|
||||
3. 后端返回 `authorization_url` 后,前端直接执行:
|
||||
|
||||
```ts
|
||||
window.location.href = authorizationURL
|
||||
```
|
||||
|
||||
浏览器随后离开 WeKnora 页面,跳转到 OIDC Provider 的授权页。
|
||||
|
||||
---
|
||||
|
||||
## 5. 后端生成授权地址
|
||||
|
||||
对应处理器:`AuthHandler.GetOIDCAuthorizationURL`
|
||||
|
||||
对应服务:`userService.GetOIDCAuthorizationURL`
|
||||
|
||||
### 5.1 参数校验
|
||||
|
||||
后端要求以下参数必须存在:
|
||||
|
||||
- `redirect_uri`
|
||||
|
||||
否则直接返回校验错误。
|
||||
|
||||
### 5.2 读取 OIDC 配置
|
||||
|
||||
`getOIDCConfig()` 会执行以下逻辑:
|
||||
|
||||
1. 检查 `OIDCAuth.Enable` 是否为 `true`;
|
||||
2. 设置默认值:
|
||||
- `ProviderDisplayName` 默认是 `OIDC`
|
||||
- `Scopes` 默认是 `openid profile email`
|
||||
- `UserInfoMapping.Username` 默认是 `name`
|
||||
- `UserInfoMapping.Email` 默认是 `email`
|
||||
3. 若未显式配置授权/令牌端点,则通过 `discovery_url` 拉取 OIDC Discovery 文档;
|
||||
4. 自动补齐:
|
||||
- `authorization_endpoint`
|
||||
- `token_endpoint`
|
||||
- `userinfo_endpoint`
|
||||
|
||||
### 5.3 生成 state
|
||||
|
||||
后端不会把 state 只当成随机串,而是编码了一个 JSON 结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"nonce": "随机字符串",
|
||||
"redirect_uri": "后端回调地址"
|
||||
}
|
||||
```
|
||||
|
||||
然后再做 base64url 编码,作为 `state` 传给 Provider。
|
||||
|
||||
这样在 OIDC Provider 回调时,后端就可以从 `state` 里还原:
|
||||
|
||||
- 本次使用的后端 `redirect_uri`
|
||||
|
||||
### 5.4 拼接授权地址
|
||||
|
||||
后端最终拼接的参数包含:
|
||||
|
||||
- `response_type=code`
|
||||
- `client_id`
|
||||
- `redirect_uri`
|
||||
- `scope`
|
||||
- `state`
|
||||
|
||||
然后返回给前端:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"provider_display_name": "Dex",
|
||||
"authorization_url": "...",
|
||||
"state": "..."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. OIDC Provider 回调到后端
|
||||
|
||||
OIDC Provider 完成认证后,会回调:
|
||||
|
||||
```text
|
||||
GET /api/v1/auth/oidc/callback
|
||||
```
|
||||
|
||||
处理器为 `AuthHandler.OIDCRedirectCallback`。
|
||||
|
||||
### 6.1 Provider 返回错误时
|
||||
|
||||
如果 query 中带有:
|
||||
|
||||
- `error`
|
||||
- `error_description`
|
||||
|
||||
后端不会返回 JSON,而是直接 302 到前端首页 `/`,并带上 hash:
|
||||
|
||||
```text
|
||||
#oidc_error=...&oidc_error_description=...
|
||||
```
|
||||
|
||||
### 6.2 解析 state
|
||||
|
||||
后端会把 `state` 做 base64url 解码并解析为结构体。
|
||||
|
||||
如果出现以下情况,将判定失败:
|
||||
|
||||
- `state` 无法解码
|
||||
- JSON 结构非法
|
||||
- `state.redirect_uri` 为空
|
||||
|
||||
失败时会重定向到前端首页:
|
||||
|
||||
```text
|
||||
#oidc_error=invalid_state
|
||||
```
|
||||
|
||||
### 6.3 校验 code
|
||||
|
||||
如果没有收到 `code`,则重定向:
|
||||
|
||||
```text
|
||||
#oidc_error=missing_code
|
||||
```
|
||||
|
||||
### 6.4 正式执行 OIDC 登录
|
||||
|
||||
若 `state` 与 `code` 都合法,则调用:
|
||||
|
||||
```go
|
||||
LoginWithOIDC(ctx, code, decodedState.RedirectURI, h.resolveDefaultTenantMode(ctx))
|
||||
```
|
||||
|
||||
注意这里传入的是 **state 中保存的 redirect_uri**,而不是重新拼接的地址,这样保证了 code 交换时使用的 `redirect_uri` 和授权时完全一致。
|
||||
|
||||
第四个参数是首次自动开户时的默认空间策略,由 `resolveDefaultTenantMode` 从 `auth.default_tenant_mode`(SystemSetting,DB > `WEKNORA_AUTH_DEFAULT_TENANT_MODE` > 配置文件)解析,与本地密码注册共用同一开关;OIDC 不再有独立的 `OIDC_AUTH_DEFAULT_TENANT_MODE`。
|
||||
|
||||
---
|
||||
|
||||
## 7. 后端用 code 换 token 并解析用户身份
|
||||
|
||||
核心逻辑位于 `internal/application/service/user.go`。
|
||||
|
||||
## 7.1 换取 OIDC token
|
||||
|
||||
`exchangeOIDCCode()` 会向 OIDC Provider 的 `token_endpoint` 发起:
|
||||
|
||||
```text
|
||||
POST application/x-www-form-urlencoded
|
||||
```
|
||||
|
||||
表单参数包括:
|
||||
|
||||
- `grant_type=authorization_code`
|
||||
- `code`
|
||||
- `redirect_uri`
|
||||
- `client_id`
|
||||
- `client_secret`
|
||||
|
||||
期望返回字段:
|
||||
|
||||
- `access_token`
|
||||
- `id_token`
|
||||
- `token_type`
|
||||
|
||||
如果 `access_token` 和 `id_token` 都缺失,则认为失败。
|
||||
|
||||
## 7.2 解析用户信息
|
||||
|
||||
`resolveOIDCUserInfo()` 的处理顺序是:
|
||||
|
||||
1. 如果有 `id_token`,先本地解码 JWT payload,提取 claims;
|
||||
2. 如果配置了 `userinfo_endpoint` 且有 `access_token`,再调用 userinfo 接口;
|
||||
3. 将两部分 claims 合并,userinfo 的字段可覆盖前面已取到的字段;
|
||||
4. 根据配置的 `user_info_mapping` 提取:
|
||||
- 用户名字段
|
||||
- 邮箱字段
|
||||
|
||||
默认映射:
|
||||
|
||||
- `username -> name`
|
||||
- `email -> email`
|
||||
|
||||
另外还有回退逻辑:
|
||||
|
||||
1. 若 username 为空,尝试 `preferred_username`
|
||||
2. 再尝试 `name`
|
||||
3. 再尝试从邮箱前缀生成用户名
|
||||
|
||||
如果最终没有拿到邮箱,则直接报错,因为本地用户是按 email 关联的。
|
||||
|
||||
---
|
||||
|
||||
## 8. 本地用户关联与自动开通
|
||||
|
||||
### 8.1 通过邮箱查找用户
|
||||
|
||||
后端使用 OIDC 返回的邮箱执行:
|
||||
|
||||
```go
|
||||
userRepo.GetUserByEmail(ctx, userInfo.Email)
|
||||
```
|
||||
|
||||
### 8.2 用户不存在时自动注册
|
||||
|
||||
若本地不存在该邮箱用户,则调用 `provisionOIDCUser()` 自动创建账号。
|
||||
|
||||
自动创建逻辑包括:
|
||||
|
||||
1. 根据 OIDC 用户名/邮箱生成本地用户名;
|
||||
2. 若用户名冲突,则自动追加 `-1`、`-2` 等后缀;
|
||||
3. 生成随机密码;
|
||||
4. 调用现有 `Register()` 流程创建用户;
|
||||
5. `Register()` 内部还会自动创建默认空间。
|
||||
|
||||
因此,**首次使用 OIDC 登录的用户,不需要提前在 WeKnora 中手工建号**。
|
||||
|
||||
### 8.3 用户禁用处理
|
||||
|
||||
如果找到的本地用户 `IsActive=false`,则登录失败,回调给前端的错误信息为:
|
||||
|
||||
```text
|
||||
Account is disabled
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 生成 WeKnora 本地登录态
|
||||
|
||||
OIDC 登录成功后,后端不会直接把 OIDC token 交给前端使用,而是继续执行:
|
||||
|
||||
```go
|
||||
GenerateTokens(ctx, user)
|
||||
```
|
||||
|
||||
生成两类 JWT:
|
||||
|
||||
- `token`:访问令牌,默认 24 小时
|
||||
- `refresh_token`:刷新令牌,默认 7 天
|
||||
|
||||
并写入本地 `auth_tokens` 存储(通过 `tokenRepo.CreateToken`)。
|
||||
|
||||
最终后端会返回 OIDC 回调载荷,其中包含:
|
||||
|
||||
- `token`
|
||||
- `refresh_token`
|
||||
- `success`
|
||||
- `message`
|
||||
- `is_new_user`
|
||||
|
||||
这一步意味着:
|
||||
|
||||
> OIDC 只负责“确认你是谁”,WeKnora 自己负责“签发系统内可用的业务令牌”。
|
||||
|
||||
---
|
||||
|
||||
## 10. 后端如何把结果传回前端
|
||||
|
||||
`OIDCRedirectCallback` 在拿到 `OIDCCallbackResponse` 后,会执行:
|
||||
|
||||
1. 将响应对象 JSON 序列化;
|
||||
2. 用 base64url 编码;
|
||||
3. 302 重定向到:
|
||||
|
||||
```text
|
||||
/#oidc_result=ENCODED_PAYLOAD
|
||||
```
|
||||
|
||||
失败时则返回:
|
||||
|
||||
```text
|
||||
/#oidc_error=...&oidc_error_description=...
|
||||
```
|
||||
|
||||
这里使用 hash 的好处是:
|
||||
|
||||
- 不会把结果作为 query 参数再次发送给服务端;
|
||||
- 前端可以在浏览器本地直接读取并清理;
|
||||
- 避免刷新时重复向后端暴露这些参数。
|
||||
|
||||
---
|
||||
|
||||
## 11. 前端如何消费 OIDC 回调结果
|
||||
|
||||
前端不是在 `Login.vue` 中处理回调,而是在 `frontend/src/App.vue` 中统一处理。
|
||||
|
||||
这样即使后端把用户重定向到 `/`,应用根组件也能接住这次 OIDC 登录结果。
|
||||
|
||||
## 11.1 App.vue 解析 hash
|
||||
|
||||
应用挂载时执行:
|
||||
|
||||
```ts
|
||||
handleGlobalOIDCCallback()
|
||||
```
|
||||
|
||||
它会读取:
|
||||
|
||||
```ts
|
||||
window.location.hash
|
||||
```
|
||||
|
||||
并解析以下字段:
|
||||
|
||||
- `oidc_error`
|
||||
- `oidc_error_description`
|
||||
- `oidc_result`
|
||||
|
||||
## 11.2 错误分支
|
||||
|
||||
如果存在 `oidc_error`:
|
||||
|
||||
1. 调用 `clearOIDCCallbackState('/login')` 清理 URL;
|
||||
2. 跳转到 `/login`;
|
||||
3. 弹出错误消息。
|
||||
|
||||
## 11.3 成功分支
|
||||
|
||||
如果存在 `oidc_result`:
|
||||
|
||||
1. base64url 解码并反序列化;
|
||||
2. 如果 `response.success=true`:
|
||||
- 先写入 `authStore.setToken(...)`
|
||||
- 再写入 `authStore.setRefreshToken(...)`
|
||||
- 随后调用 `/api/v1/auth/me`
|
||||
- 用 `/auth/me` 返回的 `user` / `tenant` 执行 `authStore.setUser(...)` 与 `authStore.setTenant(...)`
|
||||
3. 最终跳转到:
|
||||
|
||||
```text
|
||||
/platform/knowledge-bases
|
||||
```
|
||||
|
||||
这与普通账号密码登录成功后的持久化逻辑保持一致。
|
||||
|
||||
---
|
||||
|
||||
## 12. 关键配置项
|
||||
|
||||
OIDC 配置定义位于 `internal/config/config.go`,环境变量示例见 `.env.example`。
|
||||
|
||||
### 12.1 主要配置项
|
||||
|
||||
| 配置项 | 说明 |
|
||||
|---|---|
|
||||
| `OIDC_AUTH_ENABLE` | 是否启用 OIDC 登录 |
|
||||
| `OIDC_AUTH_ISSUER_URL` | Issuer 地址,可用于自动拼 discovery URL |
|
||||
| `OIDC_AUTH_DISCOVERY_URL` | OIDC Discovery 地址 |
|
||||
| `OIDC_AUTH_PROVIDER_DISPLAY_NAME` | 前端按钮显示名称 |
|
||||
| `OIDC_AUTH_CLIENT_ID` | OIDC Client ID |
|
||||
| `OIDC_AUTH_CLIENT_SECRET` | OIDC Client Secret |
|
||||
| `OIDC_AUTH_AUTHORIZATION_ENDPOINT` | 授权端点,可选 |
|
||||
| `OIDC_AUTH_TOKEN_ENDPOINT` | Token 端点,可选 |
|
||||
| `OIDC_AUTH_USER_INFO_ENDPOINT` | UserInfo 端点,可选 |
|
||||
| `OIDC_AUTH_SCOPES` | Scope 列表,默认 `openid profile email` |
|
||||
| `OIDC_USER_INFO_MAPPING_USER_NAME` | claims 中映射到用户名的字段名 |
|
||||
| `OIDC_USER_INFO_MAPPING_EMAIL` | claims 中映射到邮箱的字段名 |
|
||||
|
||||
### 12.2 启用时的最小要求
|
||||
|
||||
当 `OIDC_AUTH_ENABLE=true` 时,后端校验要求:
|
||||
|
||||
1. 必须有 `client_id`
|
||||
2. 必须有 `client_secret`
|
||||
3. 必须满足以下二选一:
|
||||
- 配置 `discovery_url`
|
||||
- 或同时配置 `authorization_endpoint + token_endpoint`
|
||||
|
||||
---
|
||||
|
||||
## 13. 本地联调示例(Dex)
|
||||
[Dex](https://dexidp.io/) 是一个简单易用的OIDC Provider,您可以通过它对接多种第三方认证系统(如OAuth2.0,Google,GitHub,LDAP等)。除了Dex之外,您也可以选择[KeyCloak](https://www.keycloak.org/)等其他符合OpenID Connect协议的Provider进行接入。
|
||||
|
||||
项目中已提供 Dex 示例配置:`misc/dex-config.yaml`。
|
||||
|
||||
其中静态客户端配置示例:
|
||||
|
||||
```yaml
|
||||
staticClients:
|
||||
- id: weknora
|
||||
redirectURIs:
|
||||
- 'http://127.0.0.1:5173/api/v1/auth/oidc/callback'
|
||||
- 'http://127.0.0.1/api/v1/auth/oidc/callback'
|
||||
name: 'WeKnora'
|
||||
# secret: <YOUR_SECRET_HERE>
|
||||
```
|
||||
|
||||
这说明本地调试时,需要确保 **Provider 注册的 redirect URI 与前端实际传给后端的 `redirect_uri` 完全一致**。
|
||||
|
||||
前端当前实现中使用的是:
|
||||
|
||||
```ts
|
||||
${window.location.origin}/api/v1/auth/oidc/callback
|
||||
```
|
||||
|
||||
所以:
|
||||
|
||||
- 若前端从 `http://127.0.0.1:5173` 访问,则 redirect URI 为
|
||||
`http://127.0.0.1:5173/api/v1/auth/oidc/callback`
|
||||
- 若通过 Nginx 统一入口访问,则可能是
|
||||
`http://127.0.0.1/api/v1/auth/oidc/callback`
|
||||
|
||||
Provider 必须提前把这些地址加入白名单。
|
||||
|
||||
---
|
||||
|
||||
## 14. 调用链路总结
|
||||
|
||||
可以把当前 OIDC 登录理解为以下 4 个阶段:
|
||||
|
||||
### 阶段一:前端发现能力
|
||||
|
||||
前端调用 `/auth/oidc/config`,决定是否展示第三方登录入口。
|
||||
|
||||
### 阶段二:浏览器跳转授权
|
||||
|
||||
前端调用 `/auth/oidc/url` 获取授权地址,然后跳转到 OIDC Provider。
|
||||
|
||||
### 阶段三:后端完成身份兑换
|
||||
|
||||
Provider 回调后端 `/auth/oidc/callback`,后端用 `code` 换 token、拉取用户信息、关联或创建本地用户,并签发 WeKnora JWT。
|
||||
|
||||
### 阶段四:前端接收最终结果
|
||||
|
||||
后端 302 回前端,并通过 `#oidc_result` 传递登录结果;前端在 `App.vue` 中统一解析,写入本地登录态并进入业务页面。
|
||||
|
||||
---
|
||||
|
||||
## 15. 注意事项
|
||||
|
||||
1. **`redirect_uri` 必须严格匹配** Provider 客户端配置。
|
||||
2. **前端首页不是 OIDC Provider 回调地址**。
|
||||
- Provider 回调的是后端 `/api/v1/auth/oidc/callback`
|
||||
- 后端再固定重定向到前端首页 `/`
|
||||
3. **邮箱是本地账号关联主键**。
|
||||
- 若 Provider 没返回 email,将无法完成登录。
|
||||
4. **首次 OIDC 登录会自动创建用户和默认空间**。
|
||||
5. **真正用于访问 WeKnora API 的仍是本地 JWT**,不是 OIDC access token。
|
||||
6. 当前实现对 `state` 做了编码封装,但 **没有服务端持久化 state/nonce 校验**;它主要用于传递上下文和基本防错,而不是完整的防重放机制。
|
||||
|
||||
---
|
||||
|
||||
## 16. 相关源码定位
|
||||
|
||||
- 前端是否展示 OIDC 登录按钮:
|
||||
- `frontend/src/views/auth/Login.vue`
|
||||
- 前端获取授权地址:
|
||||
- `frontend/src/api/auth/index.ts`
|
||||
- `frontend/src/views/auth/Login.vue`
|
||||
- 前端解析回调 hash:
|
||||
- `frontend/src/App.vue`
|
||||
- OIDC 接口路由注册:
|
||||
- `internal/router/router.go`
|
||||
- OIDC HTTP 处理:
|
||||
- `internal/handler/auth.go`
|
||||
- OIDC 业务逻辑:
|
||||
- `internal/application/service/user.go`
|
||||
- OIDC 配置结构与环境变量覆盖:
|
||||
- `internal/config/config.go`
|
||||
- Dex 本地示例:
|
||||
- `misc/dex-config.yaml`
|
||||
@@ -0,0 +1,493 @@
|
||||
# 常见问题
|
||||
|
||||
## 1. 如何查看日志?
|
||||
```bash
|
||||
docker compose logs -f app docreader postgres
|
||||
```
|
||||
|
||||
## 2. 如何启动和停止服务?
|
||||
```bash
|
||||
# 启动服务
|
||||
./scripts/start_all.sh
|
||||
|
||||
# 停止服务
|
||||
./scripts/start_all.sh --stop
|
||||
|
||||
# 清空数据库
|
||||
./scripts/start_all.sh --stop && make clean-db
|
||||
```
|
||||
|
||||
## 3. 服务启动后无法正常上传文档?
|
||||
|
||||
通常是Embedding模型和对话模型没有正确被设置导致。按照以下步骤进行排查
|
||||
|
||||
1. 查看`.env`配置中的模型信息是否配置完整,其中如果使用ollama访问本地模型,需要确保本地ollama服务正常运行,同时在`.env`中的如下环境变量需要正确设置:
|
||||
```bash
|
||||
# LLM Model
|
||||
INIT_LLM_MODEL_NAME=your_llm_model
|
||||
# Embedding Model
|
||||
INIT_EMBEDDING_MODEL_NAME=your_embedding_model
|
||||
# Embedding模型向量维度
|
||||
INIT_EMBEDDING_MODEL_DIMENSION=your_embedding_model_dimension
|
||||
# Embedding模型的ID,通常是一个字符串
|
||||
INIT_EMBEDDING_MODEL_ID=your_embedding_model_id
|
||||
```
|
||||
|
||||
如果是通过remote api访问模型,则需要额外提供对应的`BASE_URL`和`API_KEY`:
|
||||
```bash
|
||||
# LLM模型的访问地址
|
||||
INIT_LLM_MODEL_BASE_URL=your_llm_model_base_url
|
||||
# LLM模型的API密钥,如果需要身份验证,可以设置
|
||||
INIT_LLM_MODEL_API_KEY=your_llm_model_api_key
|
||||
# Embedding模型的访问地址
|
||||
INIT_EMBEDDING_MODEL_BASE_URL=your_embedding_model_base_url
|
||||
# Embedding模型的API密钥,如果需要身份验证,可以设置
|
||||
INIT_EMBEDDING_MODEL_API_KEY=your_embedding_model_api_key
|
||||
```
|
||||
|
||||
当需要重排序功能时,需要额外配置Rerank模型,具体配置如下:
|
||||
```bash
|
||||
# 使用的Rerank模型名称
|
||||
INIT_RERANK_MODEL_NAME=your_rerank_model_name
|
||||
# Rerank模型的访问地址
|
||||
INIT_RERANK_MODEL_BASE_URL=your_rerank_model_base_url
|
||||
# Rerank模型的API密钥,如果需要身份验证,可以设置
|
||||
INIT_RERANK_MODEL_API_KEY=your_rerank_model_api_key
|
||||
```
|
||||
|
||||
2. 查看主服务日志,是否有`ERROR`日志输出
|
||||
|
||||
## 4. 没有图片或者显示无效的图片链接?
|
||||
|
||||
当使用多模态功能时,如果遇到图片无法显示或显示无效链接的问题,请按照以下步骤排查:
|
||||
|
||||
### 1. 确认多模态功能已正确配置
|
||||
|
||||
在知识库设置中开启**高级设置 - 多模态功能**,并在界面中配置相应的多模态模型。
|
||||
|
||||
### 2. 确认 MinIO 服务已启动
|
||||
|
||||
如果多模态功能配置使用的是 MinIO 存储,需要确保 MinIO 镜像已正确启动:
|
||||
|
||||
```bash
|
||||
# 启动 MinIO 服务
|
||||
docker-compose --profile minio up -d
|
||||
|
||||
# 或者启动完整服务(包括 MinIO、Neo4j、Qdrant)
|
||||
docker-compose --profile full up -d
|
||||
```
|
||||
|
||||
### 3. 检查 MinIO Bucket 权限
|
||||
|
||||
确保 MinIO 对应的 bucket 具有正确的读写权限:
|
||||
|
||||
1. 访问 MinIO 控制台:`http://localhost:9001`(默认端口)
|
||||
2. 使用 `.env` 中配置的 `MINIO_ACCESS_KEY_ID` 和 `MINIO_SECRET_ACCESS_KEY` 登录
|
||||
3. 进入对应的 bucket,检查并设置访问策略为**公开读取**或**公开读写**
|
||||
|
||||
**重要提示**:
|
||||
- Bucket 名称不要包含特殊字符(包括中文),建议使用小写字母、数字和连字符
|
||||
- 如果无法修改现有 bucket 的权限,可以在配置中填入一个不存在的 bucket 名称,本项目会自动创建对应的 bucket 并设置好正确的权限
|
||||
|
||||
### 4. 配置 MINIO_PUBLIC_ENDPOINT
|
||||
|
||||
在 `docker-compose.yml` 文件中,`MINIO_PUBLIC_ENDPOINT` 变量默认配置为 `http://localhost:9000`。
|
||||
|
||||
**重要提示**:如果你需要从其他设备或容器访问图片,`localhost` 可能无法正常工作,需要将其替换为本机的实际 IP 地址:
|
||||
|
||||
|
||||
## 5. 平台兼容性说明
|
||||
|
||||
**重要提示**:`OCR_BACKEND=paddle` 模式在部分平台上可能无法正常运行。如果遇到 PaddleOCR 启动失败的问题,请选择以下解决方案
|
||||
|
||||
### 方案一:关闭 OCR 识别
|
||||
|
||||
在 `docker-compose.yml` 文件的 `docreader` 服务中删除 `OCR_BACKEND` 配置,然后重启 docreader 服务
|
||||
|
||||
**注意**:设置为 `no_ocr` 后,文档解析将不会使用 OCR 功能,这可能会影响图片和扫描文档的文字识别效果。
|
||||
|
||||
### 方案二:使用外部 OCR 模型(推荐)
|
||||
|
||||
如果需要 OCR 功能,可以使用外部的视觉语言模型(VLM)来替代 PaddleOCR。在 `docker-compose.yml` 文件的 `docreader` 服务中配置:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- OCR_BACKEND=vlm
|
||||
- OCR_API_BASE_URL=${OCR_API_BASE_URL:-}
|
||||
- OCR_API_KEY=${OCR_API_KEY:-}
|
||||
- OCR_MODEL=${OCR_MODEL:-}
|
||||
```
|
||||
|
||||
然后重启 docreader 服务
|
||||
|
||||
**优势**:使用外部 OCR 模型可以获得更好的识别效果,且不受平台限制。
|
||||
|
||||
## 6. 如何使用数据分析功能?
|
||||
|
||||
在使用数据分析功能前,请确保智能体已配置相关工具:
|
||||
|
||||
1. **智能推理**:需在工具配置中勾选以下两个工具:
|
||||
- 查看数据元信息
|
||||
- 数据分析
|
||||
|
||||
2. **快速问答智能体**:无需手动选择工具,即可直接进行简单的数据查询操作。
|
||||
|
||||
### 注意事项与使用规范
|
||||
|
||||
1. **支持的文件格式**
|
||||
- 目前仅支持 **CSV** (`.csv`) 和 **Excel** (`.xlsx`, `.xls`) 格式的文件。
|
||||
- 对于复杂的 Excel 文件,如果读取失败,建议将其转换为标准的 CSV 格式后重新上传。
|
||||
|
||||
2. **查询限制**
|
||||
- 仅支持 **只读查询**,包括 `SELECT`, `SHOW`, `DESCRIBE`, `EXPLAIN`, `PRAGMA` 等语句。
|
||||
- 禁止执行任何修改数据的操作,如 `INSERT`, `UPDATE`, `DELETE`, `CREATE`, `DROP` 等。
|
||||
|
||||
## 7. 页面里刚保存的配置几秒后又消失了?
|
||||
|
||||
这类问题通常不是配置真的被系统清掉了,而是浏览器代理、缓存或插件干扰导致前端读到了异常响应,页面随后又被旧状态覆盖。
|
||||
|
||||
建议按下面顺序排查:
|
||||
|
||||
1. 先关闭浏览器代理、抓包工具、自动改写请求的插件,再重新打开页面。
|
||||
2. 确认浏览器没有把 `localhost` 或当前访问域名走代理;如果配置了 PAC,请将 `localhost`、`127.0.0.1` 和实际部署域名加入直连名单。
|
||||
3. 强制刷新页面,或直接使用无痕窗口重新登录后再保存一次配置。
|
||||
4. 打开浏览器开发者工具的 `Network` 面板,确认保存配置相关请求返回的是最新内容,且没有被代理改写、缓存命中或重定向到其他环境。
|
||||
5. 如果是调试模式部署,可尝试重启 `app` 服务后再验证一次:
|
||||
|
||||
```bash
|
||||
docker compose restart app
|
||||
```
|
||||
|
||||
如果重启后短时间恢复正常,但再次访问又出现相同现象,仍应优先检查浏览器代理、缓存和多环境串连问题,而不是直接判断为后端配置丢失。
|
||||
|
||||
## 8. SSRF 校验白名单(`SSRF_WHITELIST`)
|
||||
|
||||
可选配置。在 `.env` 中设置 `SSRF_WHITELIST`,用于在 URL 校验等环节将指定目标加入白名单,从而绕过常规 SSRF 限制。值为逗号分隔的多条规则,每条可以是:
|
||||
|
||||
- **精确域名**:如 `api.internal`
|
||||
- **通配域名**:如 `*.example.com`
|
||||
- **IPv4**:如 `203.0.113.5`
|
||||
- **IPv6**:如 `2001:db8::1`(不要带方括号)
|
||||
- **CIDR**:如 `10.0.0.0/8`、`2001:db8::/32`
|
||||
|
||||
列入白名单的地址会在 URL 校验等处绕过常规 SSRF 规则,**生产环境请谨慎配置**,仅加入确实需要且可信的目标。
|
||||
|
||||
示例(与 `.env.example` 一致,可按需取消注释并修改):
|
||||
|
||||
```bash
|
||||
# SSRF_WHITELIST=internal.service,*.corp.example,172.16.0.0/12,2001:db8::1,fd00::/8
|
||||
```
|
||||
|
||||
|
||||
## 9. 如何开启和查看 Langfuse 可观测性追踪?
|
||||
|
||||
WeKnora 支持通过 Langfuse 对 Agent 的 ReAct 循环、大模型 Token 消耗、工具调用以及异步任务流水线进行全链路追踪。
|
||||
|
||||
**开启步骤**:
|
||||
1. 准备一个可用的 Langfuse 实例(支持云端版或私有部署版)。
|
||||
2. 在 `.env` 文件中配置以下环境变量:
|
||||
```bash
|
||||
LANGFUSE_PUBLIC_KEY=pk-lf-...
|
||||
LANGFUSE_SECRET_KEY=sk-lf-...
|
||||
LANGFUSE_HOST=https://cloud.langfuse.com # 或你的私有部署地址
|
||||
```
|
||||
3. 重启服务后,系统会自动对所有支持的模型调用和 Agent 运行轨迹进行追踪,你可以在 Langfuse 的 Traces 面板中直观地看到每次对话和后台任务的详细执行瀑布图与 Token 统计。
|
||||
|
||||
## 10. 什么是 Wiki 模式?如何使用?
|
||||
|
||||
Wiki 模式允许 Agent 根据原始文档自动生成并维护一套结构化、相互链接的 Markdown Wiki 知识库,从而实现复杂知识的体系化沉淀和图谱化。
|
||||
|
||||
**使用方法**:
|
||||
1. 进入指定**知识库的设置** -> **索引策略 (Indexing Strategy)**。
|
||||
2. 开启 **Wiki** 索引功能(可同时结合开启**知识图谱**)。
|
||||
3. 当你向该知识库上传文档时,系统会自动触发异步任务,通过大模型提取文档中的实体与核心概念,并自动生成结构化的 Wiki 页面及页面间的知识图谱链接。
|
||||
4. 你可以在该知识库的“Wiki”标签页中,使用专用的 Wiki 浏览器查阅、管理页面,并通过可视化的知识图谱查看不同内容之间的关联关系。
|
||||
|
||||
## 11. 升级到 0.6.0 后,原本能做的操作变成了「权限不足」?
|
||||
|
||||
0.6.0 引入了空间内 RBAC(角色矩阵 + 资源归属),所有写入接口都会按角色 + `creator_id` 鉴权。常见现象:
|
||||
|
||||
- **看得到但点不动**:你大概率是该资源的 `Viewer` 或非创建者的 `Contributor`,UI 已经把写操作隐藏/置灰。检查 **用户菜单 → 当前工作区** 角色徽章。
|
||||
- **共享空间里的 KB / Agent**:他人共享给你的 KB 默认按 `Viewer` 看待;要写需要在源空间里被授予 `Admin+`。
|
||||
- **API Key 调用**:`X-API-Key` 合成虚拟用户固定为所属空间的 `Admin`(仅删除空间需 `Owner`),脚本一般无需迁移。
|
||||
- **跨空间超管**:要 `User.CanAccessAllTenants=true` 且 `enable_cross_tenant_access=true`,并通过 `X-Tenant-ID` 切空间。
|
||||
|
||||
如需临时回退到「仅审计、不拦截」灰度窗口,可在配置里设置 `tenant.enable_rbac=false`(或环境变量 `WEKNORA_TENANT_ENABLE_RBAC=false`)。完整的角色矩阵和归属链请见 [`docs/RBAC说明.md`](./RBAC说明.md)。
|
||||
|
||||
## 12. 为什么登录后没有自动回到上次的工作区?
|
||||
|
||||
升级到 0.6.0 后系统会记住「最后活跃工作区」并在登录后自动恢复。若仍未恢复,通常是:
|
||||
|
||||
1. 浏览器清理了 LocalStorage / 切换了浏览器;
|
||||
2. 你最后访问的那个工作区已经把你移除(`/leave` 或被管理员剔除)— 系统会回退到默认空间;
|
||||
3. JWT 中携带了 `tenant_id` 但已无效 — 退出重登录即可。
|
||||
|
||||
## 13. 如何让多人协作时正确分配权限?
|
||||
|
||||
按照 [`docs/RBAC说明.md`](./RBAC说明.md) 的角色矩阵:
|
||||
|
||||
- 只读用户 → `Viewer`
|
||||
- 普通成员(上传文档、维护「自己」的 KB / Agent)→ `Contributor`
|
||||
- 运维人员(管理共享模型、向量库、解析器等基础设施)→ `Admin`
|
||||
- 空间所有者(拥有删除空间权限;每空间至少一位,可以有多位,最后一位不能被降级或移除)→ `Owner`
|
||||
|
||||
如果你希望开启「invite-only」(不允许自助注册到本空间),可在空间设置里打开邀请制,并通过「邀请」入口签发邀请码或链接。
|
||||
|
||||
## 14. 文档解析卡在「处理中」/ 解析追踪时间线打不开怎么办?
|
||||
|
||||
0.6.1 起每个文档解析都会记录一棵 Langfuse 风格的 Span 树(`knowledge_processing_spans` 表),可在知识库卡片菜单或卡片上的「Trace」入口打开侧边时间线,逐阶段查看进度。常见情况:
|
||||
|
||||
- **文档长时间停在「处理中」**:先打开时间线看是哪个阶段没有推进(解析 / 切分 / 向量化 / 后处理)。0.6.1 已修复多数「卡死」场景,并加入看门狗轮询;如确认是某次解析挂死,可在时间线面板点击「中止解析」,文档会进入 finalizing 后处理状态后结束。
|
||||
- **时间线一直显示「更新中」但无数据**:通常是轮询请求静默失败(网络 / 反向代理截断 SSE)。0.6.1 会显式暴露轮询失败,刷新页面或检查 Nginx 是否缓冲了响应即可。
|
||||
- **升级后没有时间线数据**:确认数据库迁移 `000055_knowledge_processing_spans`、`000056_knowledge_pending_subtasks` 已执行(服务启动会自动迁移)。
|
||||
|
||||
## 15. 如何启用 OpenSearch 作为向量库?
|
||||
|
||||
0.6.1 新增了 OpenSearch 向量库驱动(k-NN)。在 **设置 → 向量库** 中新增 OpenSearch 引擎并填写连接地址、凭据即可;KB 可绑定该向量库。注意:
|
||||
|
||||
- 连接地址会经过 SSRF 策略校验,内网 / 回环地址需符合放行规则;可用「测试连接」先行校验。
|
||||
- 集成测试与索引映射细节见 [`docs/dev/opensearch-integration-test.md`](./dev/opensearch-integration-test.md)。
|
||||
|
||||
## 16. 内置模型(builtin models)如何用 YAML 声明式管理?
|
||||
|
||||
0.6.1 起平台内置模型由 `config/builtin_models.yaml` 声明式驱动,支持 `${ENV}` 变量插值,并通过 `managed_by` 字段与漂移巡检保持数据库与 YAML 一致。常见问题:
|
||||
|
||||
- **改了 YAML 不生效**:内置模型在服务启动时做生命周期对账(drift sweep);确认重启了服务,且条目通过了 schema 校验(ID 长度、必填字段)。
|
||||
- **Docker 下环境变量未注入**:`builtin_models` 依赖 `env_file` 数组形式注入变量,确认 compose 中按数组形式挂载了 `.env`。
|
||||
- 参考样例:`config/builtin_models.yaml.example`。
|
||||
|
||||
## 17. 系统管理员(System Admin)与平台设置怎么用?
|
||||
|
||||
0.6.1 引入了系统管理员与统一平台设置面板(含平台审计日志),与空间内 RBAC 区分:系统管理员管理的是「平台级」配置,而非单个空间内的资源。首次启用需通过系统管理员 bootstrap 流程晋升首个管理员;撤销管理员权限有安全防护(避免误撤导致无人可管)。相关迁移为 `000053_system_admin_and_settings`。
|
||||
|
||||
## 18. 上传时如何自定义解析配置(process_config)?
|
||||
|
||||
0.6.2 起,文件 / URL / 文件夹上传可携带 `process_config`(`KnowledgeProcessOverrides`),在**本次批次**内覆盖知识库默认的解析引擎、分块、多模态(VLM / ASR)、问题生成、图谱抽取等设置,而不会改动 KB 全局配置。Web UI 在上传前会弹出确认对话框供调整;API 与 `weknora doc upload` 传同名 JSON 即可。
|
||||
|
||||
- **与 KB 默认配置的关系**:未传的字段沿用 KB 默认值;`graph_enabled` 仅在 `extract_config.enabled` 为 true 时生效。
|
||||
- **重新解析**:`POST /knowledge/:id/reparse` 可在 body 中传 `process_config` 以新配置重跑解析,覆盖项会写入 `knowledge.metadata.process_overrides`。
|
||||
- **图片 / 音频校验**:批次含图片时需 KB 已配置 VLM;含音频时需已配置 ASR,否则上传会被拒绝。
|
||||
- 详见 [`docs/api/knowledge.md`](./api/knowledge.md)。
|
||||
|
||||
## 19. 升级到 0.6.2 后 `weknora` CLI 登录或 MCP 工具报错?
|
||||
|
||||
0.6.2 随附 **CLI v0.9**(破坏性变更),常见迁移:
|
||||
|
||||
- **`auth login` 不再创建 profile**:先 `weknora profile add <name> --host <url> --use`,再 `weknora auth login`;切换 profile 用全局 `--profile <name>`。
|
||||
- **`auth logout` / `auth refresh` 去掉 `--name`**:作用于当前 active profile。
|
||||
- **MCP 工具 `agent_invoke` 已更名为 `session_ask`**:外部 MCP 客户端需刷新工具 schema。
|
||||
- **`agent create --kb` 改为 `--attach-kb`**;`doc delete --all` 与 `search chunks` / `search docs` 的 `--kb` 必填且支持名称或 ID。
|
||||
- 新增 `weknora session stop <session-id>` 可中止进行中的 Agent 运行;仓库内附带 `weknora-rag-search` / `weknora-shared` 内置 Skills。
|
||||
- 详见 [`cli/CHANGELOG.md`](../cli/CHANGELOG.md)。
|
||||
|
||||
## 20. pgvector 检索变慢或刚升级后需要做什么?
|
||||
|
||||
0.6.2 新增迁移 `000059_embeddings_hnsw_1024`,为 **1024 维** embedding(如 bge-m3)在 PostgreSQL pgvector 上创建 HNSW 索引。服务启动会自动执行迁移;若你使用其他维度,该索引可能不适用,需按自身 embedding 维度另行调优。升级后首次大批量入库期间索引构建可能占用额外 I/O,属正常现象。
|
||||
|
||||
## 21. 如何在网站嵌入 WeKnora 智能体(Embed Widget)?
|
||||
|
||||
0.6.3 起支持**嵌入渠道**:在 **集成中心** 或 Agent 编辑器中创建 embed 渠道,绑定自定义 Agent,获取渠道 ID 与发布 Token(`em_…`),将 `weknora-widget.js` 嵌入外部网页即可提供访客问答。
|
||||
|
||||
- **域名白名单**:必须在渠道配置中填写允许加载 Widget 的 Origin,否则 exchange 会返回 403。
|
||||
- **安全模式(推荐)**:生产环境不要把 `em_…` 写在页面 HTML 里;由业务后端提供 `token-endpoint`,用发布 Token 调 `POST /api/v1/embed/:id/exchange` 换取短时令牌 `ems_…`(约 30 分钟有效)。详见 [`docs/embed-secure-mode.md`](./embed-secure-mode.md) 与 [`docs/embed-subdomain.md`](./embed-subdomain.md)。
|
||||
- **限流**:渠道可配置每分钟 / 每日请求上限;超限返回 429。
|
||||
- **子域部署**:若 embed 页面与 API 不同子域,参考 `docs/embed-subdomain.md` 配置 CORS 与 Nginx。
|
||||
|
||||
## 22. 文档如何设置多个标签?
|
||||
|
||||
0.6.3 将文档标签从单选升级为**多标签**(迁移 `000063_knowledge_multi_tags`)。在知识库列表可为文档打多个标签,侧边栏支持按标签筛选;**标签管理**抽屉可批量维护标签。API 上传 / 更新知识时传 `tag_ids` 数组(取代旧的单 `tag_id`)。
|
||||
|
||||
## 23. 如何批量重新解析文档?
|
||||
|
||||
在知识库文档列表框选多篇文档后,使用批量操作栏的 **重新解析**;也可调用 `POST /knowledge/batch-reparse`,body 可含 `ids` 与可选 `process_config`。任务异步入队,UI 会在入队后刷新状态。单篇仍可用 `POST /knowledge/:id/reparse`。
|
||||
|
||||
## 24. RSS 数据源如何配置?
|
||||
|
||||
0.6.3 新增 **RSS / Atom** 连接器。在知识库 **设置 → 数据源** 中选择 RSS,填写 Feed URL 与同步策略即可全量 / 增量拉取正文入库。若部分条目失败,同步日志会展示 partial failure 详情;编辑数据源保存配置**不会**自动触发同步,需手动点同步。
|
||||
|
||||
## 25. MCP 远程服务如何配置 OAuth2?
|
||||
|
||||
0.6.3 支持 MCP 服务的 **OAuth2 授权**(迁移 `000062_mcp_oauth`)。在 **设置 → MCP** 添加 HTTP 类型服务并选择 OAuth2,按向导完成授权回调;另支持自定义 HTTP Header 与 JSON **代码导入**快速粘贴配置。授权 Token 加密存储,过期后需在 UI 重新授权。
|
||||
|
||||
## 26. Embedding 维度如何覆盖?
|
||||
|
||||
在 **设置 → 模型** 编辑 Embedding 模型时可填写 **dimensions** 覆盖值(如 1024、1536)。0.6.3 修复了部分提供商请求未携带 `dimensions` 的问题(#1654)。若向量库索引维度与模型不一致,检索可能异常,请保持 KB 绑定向量库与模型维度一致。
|
||||
|
||||
## 27. Agent 提示「模型未就绪」无法对话?
|
||||
|
||||
0.6.3 在 Agent 选择器引入**模型就绪校验**:绑定的 LLM / Embedding / Rerank / VLM 缺失或配置无效时会阻断对话并给出修复指引。可在模型卡片打开 **调试抽屉** 先测试连通性;确认 KB 与 Agent 引用的模型均存在且可用。
|
||||
|
||||
## 28. 如何创建并限制权限范围 API Key?
|
||||
|
||||
0.7.0 引入**权限范围 API Key 与 Principal 模型**(迁移 `000064_principal_model`、`000065_tenant_api_keys`)。API Key 不再等同于某个人类用户,而是独立的 Principal,携带显式角色与能力(capability)授权:
|
||||
|
||||
- 在 **设置 → API 集成**(Owner 可见)中创建 Key,可勾选能力(如 `manage_kbs` 覆盖 KB 全生命周期、`manage_storage_backends` 等),并可限制到指定知识库。
|
||||
- Key 的 `last_used_at` 按节流更新,避免高频写库。
|
||||
- 路由级守卫会拒绝越权访问;管理类接口对 API Key Principal 默认拒绝,请为集成使用具备对应能力的 Key,而非全权 Key。
|
||||
- MCP OAuth 与嵌入会话按 Principal 隔离,不同集成之间互不串号。
|
||||
|
||||
### 如何用一个 API Key 自动化管理多个空间?
|
||||
|
||||
SystemAdmin 可在 **系统管理 → 平台 API Key** 创建 `scope_type=platform` 的 Key。平台 Key 不绑定单一空间:调用普通空间 API 时必须携带 `X-Tenant-ID`,并继续受原有 capability 和知识库范围守卫约束;调用开放的系统控制面接口则需要对应的 `system_*` capability。平台 Key 不支持 `full_access`,也不能创建、轮换或吊销其他平台 Key。
|
||||
|
||||
## 29. 一个空间如何绑定多个对象存储实例?
|
||||
|
||||
0.7.0 支持**多实例存储后端**(迁移 `000068_storage_backends`)。一个空间可注册多个存储实例(`local` / `minio` / `cos` / `tos` / `s3` / `oss` / `ks3` / `obs`),不同知识库绑定到不同实例,空间维度还有一个默认实例:
|
||||
|
||||
- 在 **设置 → 存储后端** 创建/测试/设为默认(需 Admin+;API Key 需 `manage_storage_backends` 能力)。
|
||||
- 未显式绑定的新知识库使用空间默认实例;响应中的 `access_key_id` / `secret_access_key` 会被掩码,更新时提交掩码占位符不会覆盖库中真实凭据。
|
||||
- 若创建知识库时提示存储引擎不可用,请确认目标 provider 在 `STORAGE_ALLOW_LIST` 允许范围内。详见 [`docs/api/storage-backend.md`](./api/storage-backend.md)。
|
||||
|
||||
## 30. 后台解析/入库任务积压或需要排查失败任务怎么办?
|
||||
|
||||
0.7.0 新增系统管理员的**运行时任务队列面板**与 **Worker 池治理**。文档处理从单一聚合池改为分阶段独立池(core / 后处理 / enrichment / maintenance)+ 弹性共享池,Wiki 独立治理:
|
||||
|
||||
- 在 **系统设置 → 运行时队列** 查看队列深度、按模型并发统计、失败任务详情,并可手动重试。
|
||||
- 可通过 `WEKNORA_ASYNQ_*_CONCURRENCY` 与 `asynq.*_concurrency` 系统设置调整各池并发(需重启服务);`model.max_concurrency` 用于约束单模型后台并发。
|
||||
- 详见 [`docs/worker-pool-governance.md`](./worker-pool-governance.md)。注意:Worker 并发只是调度预算,仍受模型配额、DocReader 容量、向量库与数据库连接数限制。
|
||||
|
||||
## 31. 对话中如何临时上传图片/文档做一次性问答?
|
||||
|
||||
0.7.0 支持**会话级临时附件**(迁移 `000070_temporary_documents`)。在对话输入区上传图片或文档,系统异步解析后仅用于当前会话的问答,不会写入知识库。图片与附件共享一个合并数量上限;附件内容会在多轮对话中保留。
|
||||
|
||||
## 32. 如何接入 QQBot / Lark(飞书国际版)?
|
||||
|
||||
0.7.0 新增 **QQBot** 平台集成,并支持飞书国际版 **Lark**(区域感知路由)。在 **设置 → IM 集成** 添加对应渠道并填写凭据即可;飞书回复通过 reply-message 接口发送,回复会落在原消息线程内。
|
||||
|
||||
## 33. 如何为 Redis 启用 TLS?
|
||||
|
||||
0.7.0 支持 Redis 的 **TLS 连接**(#1930)。按环境变量启用 TLS 后,启动日志会打印 TLS 配置状态便于确认。若连接失败,请核对证书/CA 配置与 Redis 服务端是否要求 TLS。
|
||||
|
||||
## 34. 升级到 0.7.0 后 `weknora` CLI 命令找不到或行为变化?
|
||||
|
||||
0.7.0 随附 **CLI v0.10**(Agent 优先,破坏性变更):新增 `model` / `message` / `config` / `skills` 命令组,`doc reparse` / `doc update`,`kb config` / `kb config set`;`session continue` 更名为 `session resume`,新增 `session tool-approval`;提供 agent-first 的 chat 与 `session ask` 输出模式,并强化了 SSE 可靠性与类型化错误。详见 [`cli/CHANGELOG.md`](../cli/CHANGELOG.md)。
|
||||
|
||||
## 35. 如何接入云之家(Yunzhijia)?
|
||||
|
||||
0.7.1 新增 **云之家 IM 集成**。在 **设置 → IM 集成** 添加云之家渠道并填写应用凭据即可;集成基于 WebSocket 长连接接收消息,支持图片消息入库(带 SSRF 安全下载),默认以 **Markdown** 格式回复。若图片无法下载,请检查出站网络与凭据是否具备下载权限。
|
||||
|
||||
## 36. 如何使用火山引擎 Rerank / 智谱 AI 网络搜索?
|
||||
|
||||
0.7.1 新增两个供应商:
|
||||
|
||||
- **火山引擎 Rerank**:在 **设置 → 模型** 中添加 Rerank 模型并选择火山引擎。当单次请求文档数超过 API 上限时,客户端会自动分批发送并合并结果。vLLM Rerank 现默认不再发送 `truncate_prompt_tokens` 以提升兼容性。
|
||||
- **智谱 AI 网络搜索**:在 **设置 → 网络搜索** 中选择智谱 AI 作为搜索供应商并填写凭据即可,用于 Agent 联网检索。
|
||||
|
||||
## 37. 升级到 0.7.1 后对话记忆(Memory)设置消失了?还需要 Neo4j 吗?
|
||||
|
||||
0.7.1 **移除了基于 Neo4j 的会话记忆(episodic memory)** 功能,相关 API 字段、设置项与嵌入开关一并下线,对话不再依赖 Neo4j 做记忆召回。**注意:知识图谱(GraphRAG / 图检索)仍然使用 Neo4j**,因此若你启用了图谱检索,Neo4j 依旧是必需组件,无需移除部署。若你此前仅为记忆功能部署 Neo4j 且未使用图谱,可按需精简。
|
||||
|
||||
## 38. 官方文档在哪里看?如何本地或独立部署文档站?
|
||||
|
||||
0.7.2 新增了完整的官方产品文档,位于仓库 [`website-docs/`](../website-docs/README.md) 目录,按「入门 → 架构 → 功能 → API → 客户端 → 开发」六个板块组织,覆盖约 360 个 API 端点、约 150 个环境变量与 9 大扩展点。
|
||||
|
||||
该目录同时是一个 VitePress 站点,两种使用方式:
|
||||
|
||||
```bash
|
||||
# 本地预览
|
||||
cd website-docs && npm install && npm run dev
|
||||
|
||||
# 独立容器部署(容器内 Nginx 监听 8081)
|
||||
docker build -t weknora-docs website-docs
|
||||
docker run -d -p 8081:8081 weknora-docs
|
||||
```
|
||||
|
||||
站点的版本号在构建时自动读取仓库根目录的 `VERSION` 文件,因此升级版本后无需手动改文档。若某处截图显示为虚线占位框,说明 `website-docs/public/screenshots/` 下缺少同名图片,补图即可生效,不需要改 Markdown。
|
||||
|
||||
`website-docs/sample-data/` 下还提供了 4 份 Markdown 样例文档与 1 份 FAQ 导入 JSON,可以直接用来跑一遍「建库 → 上传 → 问答」;`examples/mcp-demo/` 是一个可直接运行的本地 MCP 服务示例。
|
||||
|
||||
## 39. 文件夹上传后文档标题变成了一长串路径?
|
||||
|
||||
这是 0.7.2 之前的行为:文件夹上传会把相对目录塞进 `file_name`,导致列表里显示整条路径,也无法按文件夹筛选。
|
||||
|
||||
0.7.2 把路径拆到独立的 `folder_path` 字段(迁移 `000079_knowledge_folder_path`),并**自动回填历史数据**,因此升级后已有知识库同样会呈现正确的文件夹树,无需重新上传。文档列表左侧会出现文件夹树,可以像文件管理器一样浏览、重命名文件夹,也可以通过行内的文件夹选择器把文档重新归档到其他文件夹。单独上传(非文件夹)的文档统一挂在树的根目录下。
|
||||
|
||||
## 40. 分块内容不准确,可以手动修改吗?改完会重新建索引吗?
|
||||
|
||||
可以。0.7.2 支持在界面上直接编辑检索分块(迁移 `000078_chunk_editing_and_custom_metadata`):
|
||||
|
||||
- 每次编辑都会把改动前的版本存入 `chunk_revisions`,可在「分块编辑历史」里逐版本查看 diff 并一键回滚。
|
||||
- **编辑保存后会自动重建该分块的索引**(`index_status` 字段跟踪重建状态),无需手动 reparse。
|
||||
- 分块的生成问题可以单独增删改与重新生成,且在内容编辑后仍会保留。
|
||||
- 注意:重新解析(reparse)整篇文档会按新的解析结果重建分块,此前的人工编辑不会被保留,请谨慎操作。
|
||||
|
||||
## 41. Wiki 页面被 Agent 覆盖了,能找回旧版本吗?
|
||||
|
||||
能。0.7.2 为 Wiki 页面引入版本历史(迁移 `000075_wiki_page_revisions`):页面每次被覆盖前都会留存一份快照,在 Wiki 浏览器右上角打开「版本历史」抽屉即可查看完整历史、行级 diff,并一键回滚到任意版本。每个版本都会记录来源(`pipeline` 流水线 / `agent` 修复工具 / `user` 手动编辑 / `revert` 回滚),便于判断是谁改的。页面也支持在浏览器内直接手动编辑。
|
||||
|
||||
另外,0.7.2 移除了 Wiki 浏览器里重复的操作日志(迁移 `000077_remove_wiki_log`),Wiki 的变更记录统一并入**知识库活动流**查看。
|
||||
|
||||
## 42. 第三方 App 拿到的图片链接是 `resource://...` 无法显示,怎么办?
|
||||
|
||||
默认情况下 API 返回的是内部句柄 `resource://<handle>`,客户端需要再调用带鉴权的 `/files` 代理才能取到图。0.7.2 新增直链模式,让接口直接返回可加载的 http(s) 链接:
|
||||
|
||||
- **单次请求**:在 URL 上加 `?resource_urls=public`。
|
||||
- **整个部署**:设置环境变量 `RESOURCE_URL_MODE=public`。
|
||||
|
||||
注意事项:
|
||||
|
||||
- 直链依赖 `APP_EXTERNAL_URL`(或存储后端本身公网可达)才能生成;无法生成时该引用会保持 `resource://` 原样,客户端仍可回退到 `/files`。
|
||||
- `public` 会为每个被引用文件签发**限时匿名可读**链接(WeKnora 侧 2 小时,MinIO 24 小时),请评估是否符合你的安全要求。
|
||||
- 匿名的 embed 渠道与限定了知识库范围的 API Key **始终返回 handle**,不受该变量影响。
|
||||
- 建议同时配置 `SYSTEM_AES_KEY`,以便复用 grant 行、稳定直链 URL 并降低读接口的写入压力。
|
||||
|
||||
详见 [API 文档 · 文件与图片引用](./api/README.md)。
|
||||
|
||||
## 43. 使用 AWS S3 但不想在配置里写 AK/SK?
|
||||
|
||||
0.7.2 支持 **AWS SDK 默认凭据链**(#2008):把 `S3_ACCESS_KEY` 与 `S3_SECRET_KEY` **同时留空**即可,SDK 会依次尝试 EC2/ECS/EKS 实例角色、IRSA / Web Identity、环境变量与共享配置文件。注意两者必须同时填写或同时留空,只填一个会报配置错误。`S3_ENDPOINT` 也可留空,此时使用 `S3_REGION` 对应的 AWS 标准端点。
|
||||
|
||||
## 44. MCP Server 用 `uvx` 启动失败,或者应该装哪个包?
|
||||
|
||||
请安装**官方包 `tencent-weknora-mcp`**(由 Tencent/WeKnora 仓库 CI 通过 Trusted Publishing 发布)。此前社区包 `weknora-mcp` 非官方维护,请迁移安装命令。
|
||||
|
||||
0.7.2 随附 MCP Server 1.1.x,已迁移到 mcp 2.x 的高级 `MCPServer` API,修复了 `uvx` 拉到 SDK 2.x 时的启动崩溃(`AttributeError: 'Server' object has no attribute 'list_tools'`),并恢复了 HTTP(`stateless_http`)与 SSE(`/sse/messages/`)传输的路由兼容性。工具总数为 29 个,新增 `create_knowledge_from_text`(用 Markdown 文本直接建知识条目)与 `list_shared_knowledge_bases`(共享知识库也纳入按名称解析)。
|
||||
|
||||
行为变化提醒:工具执行失败时,MCPServer 2.x 返回 `CallToolResult(isError=True)`,不再像旧版低层 API 那样以成功响应返回 `"Error executing …"` 文本前缀。只解析 `content[0].text` 的客户端通常无感,依赖 `isError` 标志的集成方行为会更符合 MCP 规范。
|
||||
|
||||
## 45. 升级到 0.8.0 后技能沙箱起不来 / 找不到 Local 后端?
|
||||
|
||||
0.8.0 **移除了 Local 宿主机进程沙箱**。技能执行改为会话级常驻沙箱,三个后端共用同一套协议:
|
||||
|
||||
- **Docker**(单机 / 私有化):默认**关闭**。本机 `docker.sock` 等同宿主机 root,需系统管理员在 **设置 → 系统设置 → 网络安全** 打开,或设置 `WEKNORA_SANDBOX_DOCKER_ENABLED=true`。打开后才会出现「添加 Docker 后端」入口;已有配置仍可查看/删除。
|
||||
- **E2B**:E2B Cloud,或任意 E2B 兼容控制面(含自托管)。
|
||||
- **CubeSandbox**:集群模板 + 网络策略。
|
||||
|
||||
原 Local 配置需要按上面任一后端重建。每个空间可配多个沙箱实例,并可为每个配置设置**网络策略**(默认放行出站、关闭公网入站;可改成默认拒绝出站再写允许名单)。详见 [`docs/sandbox-docker-backend.md`](./sandbox-docker-backend.md) 与 [`docs/sandbox-protocol.md`](./sandbox-protocol.md)。
|
||||
|
||||
## 46. 技能目录和沙箱配置是什么关系?安装一直转圈怎么办?
|
||||
|
||||
0.8.0 把技能做成空间级目录(迁移 `000086_tenant_skills` / `000090_skill_catalog`),再**按沙箱配置安装成快照**:
|
||||
|
||||
1. 在 **设置 → 技能沙箱** 建好后端配置;
|
||||
2. 从 ClawHub(`@owner/slug`)、SkillHub / skills.sh、GitHub/GitLab URL 或 zip 上传安装;
|
||||
3. 安装抽屉会保持打开并显示环形进度;卡住时用「停止安装」,再用「重新安装」走已保存的安装包。
|
||||
|
||||
环境变量分两层:**空间级**(Admin,该空间所有人共用)和**个人级**(`/api/v1/me/env-vars`,值永远不会读回)。技能声明的 `WEKNORA_*` 凭据可以按人填写。卸载沙箱里的技能不会删掉目录里的安装包。
|
||||
|
||||
## 47. 0.7.1 删了 Neo4j 会话记忆,0.8.0 的「长期记忆」是一回事吗?
|
||||
|
||||
不是。0.7.1 去掉的是旧版 **Neo4j episodic conversation memory**;**知识图谱(GraphRAG)仍然用 Neo4j**。0.8.0 的长期记忆是全新产品(迁移 `000084_memory`):
|
||||
|
||||
- 空间管理员先打开,用户还可以再关掉自己的;
|
||||
- 类型:`profile` / `preference` / `fact` / `task` / `interest`;
|
||||
- 自动抽取的条目先停在「待确认」,不会静默写进提示词;
|
||||
- 常驻画像每轮注入,情境记忆按需召回,Agent 也可用 `search_memory`;
|
||||
- 反复引用的文档会形成亲和度,检索时加权。
|
||||
|
||||
接口在 `/api/v1/memory/*`,只操作当前调用者自己的记忆,需要 full-access API Key 或登录会话。
|
||||
|
||||
## 48. 文档解析能否不经过 docreader?anydoc 是什么?
|
||||
|
||||
0.8.0 引入进程内 **anydoc** 引擎(`third_party/anydoc-go`)。当绑定已链接时,anydoc 能转换的格式(含 doc/docx/ppt/pptx)会优先走 Go 进程,不再先打到 docreader。PPT/PPTX 在没有引擎规则时仍默认 MarkItDown。官方 app 镜像默认链接 anydoc。解析失败或格式不在 anydoc 覆盖范围时,仍回退到 docreader / MarkItDown / MinerU 等既有引擎。
|
||||
|
||||
## 49. OIDC 登录提示签名无效,或想跳过前端直接 302?
|
||||
|
||||
0.8.0 会通过 JWKS **校验 ID Token 签名**(#2799),显式配置的 token endpoint 同样走这条路径。请确认 IdP 的 JWKS URL 可达、密钥已轮换到当前 kid。若要做门户级跳转,可调用 `GET /auth/oidc/start` 直接 302 到 IdP,不必先渲染 SPA 握手页。
|
||||
|
||||
## 50. 开启复杂密码后注册 / 改密失败?
|
||||
|
||||
系统设置或 `WEKNORA_AUTH_COMPLEX_PASSWORD_ENABLED=true` 打开后,密码必须同时包含大写、小写、数字和特殊字符(`!@#$%^&*()_+-=[]{}|;:,.<>?`),长度 8–32。注册、个人中心改密、管理员重置走同一套规则。未打开时仍只要求长度。
|
||||
|
||||
## P.S.
|
||||
如果以上方式未解决问题,请在issue中描述您的问题,并提供必要的日志信息辅助我们进行问题排查
|
||||
@@ -0,0 +1,263 @@
|
||||
# 空间 RBAC 说明
|
||||
|
||||
本文档介绍 WeKnora **空间内权限控制(Workspace RBAC)** 的设计、角色矩阵、资源归属模型、配置方式,以及它与 [共享空间](./共享空间说明.md) 之间的关系。
|
||||
|
||||
> 状态:已随 #1303 发布,由配置项 `tenant.enable_rbac` 控制,默认 `true`(强制鉴权)。可临时切到 `false` 进入「仅记录不拦截」的灰度窗口。
|
||||
|
||||
## 一、为什么需要 RBAC
|
||||
|
||||
在 RBAC 引入之前,只要通过 `X-API-Key` 或 JWT 认证成功,调用方在空间内基本等同于管理员。这在单人自部署场景没问题,但只要一个空间里出现两个及以上的真人成员(团队共享一套知识库),就必须区分:
|
||||
|
||||
- 谁可以删除知识库、撤销 API Key(管理员/Owner);
|
||||
- 谁可以上传文档、编辑「自己」的知识库(Contributor);
|
||||
- 谁只能读取与提问(Viewer)。
|
||||
|
||||
RBAC 在原有 JWT / API Key 认证之上,叠加了一层**空间内角色矩阵**,使三种状态都成为一等公民。
|
||||
|
||||
## 二、角色矩阵
|
||||
|
||||
每个空间成员(`tenant_members` 一行)拥有且仅拥有一个角色:
|
||||
|
||||
| 角色 | 标识 | 典型场景 | 关键能力 |
|
||||
|------|------|----------|----------|
|
||||
| 只读 | `viewer` | 只查阅、提问的成员 | 仅读,不可发起任何变更 |
|
||||
| 贡献者 | `contributor` | 上传文档、维护自己的 KB / Agent | 可变更 `creator_id == 自己` 的资源;他人资源等同 Viewer |
|
||||
| 管理员 | `admin` | 空间内运维 | 可变更空间内任意资源;管理成员;配置共享基础设施(模型、解析器、存储、向量库等) |
|
||||
| Owner | `owner` | 空间所有者 | Admin 的全部权限 + 可删除空间;每个空间**至少有一位,可以有多位** |
|
||||
|
||||
角色按 `viewer < contributor < admin < owner` 递增,高角色继承低角色权限。
|
||||
|
||||
Owner 的数量约束是“至少一位”,而不是“只能一位”。系统允许同一空间存在多个活跃 Owner;降级或移除 Owner 时,只有会导致空间失去最后一位 Owner 的操作才会被拒绝。
|
||||
|
||||
### 鉴权层的两个例外
|
||||
|
||||
- **跨空间超管**:`User.CanAccessAllTenants=true` 且 `enable_cross_tenant_access=true` 时,通过 `X-Tenant-ID` 切换到目标空间后等同 Admin,不需要在目标空间里有 `tenant_members` 行。用于多空间运营方。
|
||||
- **API Key 调用**:`X-API-Key` 合成的虚拟用户在其所属空间内固定为 Admin(仅删除空间仍需 Owner)。脚本集成无需迁移。
|
||||
- **孤儿空间自愈**:若一个空间在 `tenant_members` 表里没有任何活跃成员(典型场景:仅 API Key 使用过该空间),首位通过认证的真人会被自动晋升为 Owner,避免锁死。
|
||||
|
||||
## 三、资源归属模型
|
||||
|
||||
光有角色矩阵不够,否则 Contributor 之间可以互相破坏。为此在迁移 `000043` 中给关键表加了 `creator_id`:
|
||||
|
||||
- `knowledge_bases.creator_id` —— 老数据回填为该空间的 Owner;空串/NULL 表示「空间共有,仅 Admin+ 可变更」。
|
||||
- `custom_agents.creator_id` —— Agent 创建者。
|
||||
- `custom_agents.runnable_by_viewer` —— 默认 `true`,允许 Viewer 在对话中调用该 Agent;置 `false` 则提升到 Contributor 起步。
|
||||
|
||||
子资源沿着归属链回溯到 KB 的 `creator_id`:
|
||||
|
||||
```
|
||||
chunk_id ─► knowledge_id ─► kb_id ─► knowledge_bases.creator_id
|
||||
```
|
||||
|
||||
FAQ 条目、生成的问题、KB 标签、Wiki 页面同理。
|
||||
|
||||
由此衍生出两类守卫:
|
||||
|
||||
- **角色守卫**:`Viewer()` / `Contributor()` / `Admin()` / `Owner()` —— 只看角色。用于空间级基础设施(模型、向量库、IM 通道等)。
|
||||
- **归属守卫**:`OwnedKBOrAdmin()` / `OwnedAgentOrAdmin()` / `OwnedChunkKBOrAdmin()` …… —— 「我是这条资源的 `creator_id`」**或**「我至少是 Admin」二者满足其一即可。用于具体资源的写操作。
|
||||
|
||||
这样可以让「Contributor 在自己的 KB 里像 Owner,在别人的 KB 里像 Viewer」自然成立。
|
||||
|
||||
## 四、与共享空间的关系(重点)
|
||||
|
||||
[共享空间](./共享空间说明.md)(Organization)和空间 RBAC 解决的是**不同维度**的问题,必须同时满足才能完成一次跨空间操作:
|
||||
|
||||
| 维度 | 解决什么 | 主键模型 | 角色集合 |
|
||||
|------|---------|---------|---------|
|
||||
| **空间 RBAC** | 同一空间内「你能对自己 / 别人 / 共享基础设施做什么」 | `tenant_members(user_id, tenant_id, role)` | viewer / contributor / admin / owner |
|
||||
| **共享空间** | 跨空间「把我的 KB / Agent 让别的空间的人也用」 | `organization_members(user_id, org_id, role)` + 共享关系表 | 管理员 / 编辑者 / 只读 |
|
||||
|
||||
两者**正交**:
|
||||
|
||||
- 共享空间不持有任何 KB 或 Agent,它只是「某 KB 以某权限被共享到某空间」的关系记录。
|
||||
- 资源始终归属一个空间,归属与 `creator_id` 都不会因为共享而改变。
|
||||
- 一次对**他人共享给你**的 KB 的写操作,需要同时满足:
|
||||
1. **共享一侧**:该 KB 被以「可写」权限共享到了你和发起方都在的共享空间;
|
||||
2. **空间角色一侧**:你在该共享空间内不是「只读」(即至少是编辑者);
|
||||
3. **空间 RBAC 一侧**:你的访问通过共享路径解析为对源空间的「以共享空间身份」访问,仍要经过源空间的 RBAC 检查。具体地,访问检查会在确认 `kb.tenant_id == 你的当前空间` 不成立后,回落到共享路径校验。
|
||||
|
||||
简化的判定顺序(见 `internal/middleware/kb_access.go`):
|
||||
|
||||
```text
|
||||
┌────────────────────────┐
|
||||
│ KB 属于我当前空间? │ ──是──► 进入空间 RBAC:
|
||||
└──────┬─────────────────┘ 角色 + creator_id 决定能否写
|
||||
否
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ KB 通过共享空间分享给 │ ──否──► 403 / 404
|
||||
│ 我所在的某个空间? │
|
||||
└──────┬─────────────────┘
|
||||
是
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ 我在该空间是 viewer? │ ──是──► 只读
|
||||
│ │ ──否──► 按共享时设定的「只读/可写」执行
|
||||
└────────────────────────┘
|
||||
```
|
||||
|
||||
要点整理:
|
||||
|
||||
- **共享空间不会绕过空间 RBAC**:若一个 KB 在源空间里被标记为「仅 Admin+ 可写」(例如 `creator_id` 为空的空间共有 KB),即使共享时给了「可写」权限,外空间成员也只能读取——因为没人能跨空间成为源空间的 Admin。
|
||||
- **API Key 跨空间访问**:API Key 在所属空间内是 Admin,但**不会**因此自动获得对其他空间通过共享空间共享过来的 KB 的写权限——共享空间使用的是 `organization_members.role`,与 API Key 无关。
|
||||
- **审计也是分开的**:空间内角色变更写入 `audit_logs`(`rbac.member_*` 动作),共享空间内的成员、共享关系变更由共享空间自身的接口记录。
|
||||
|
||||
一句话总结:**空间 RBAC 是「纵向」的纵深防御,共享空间是「横向」的协作通道;任何跨空间的有写副作用的操作,都要同时穿过这两道闸口。**
|
||||
|
||||
## 五、配置
|
||||
|
||||
`config/config.yaml`:
|
||||
|
||||
```yaml
|
||||
tenant:
|
||||
# 默认 true,强制鉴权。改为 false 进入「仅记录不拦截」灰度窗口
|
||||
enable_rbac: true
|
||||
# 跨空间超管开关,默认 false
|
||||
enable_cross_tenant_access: false
|
||||
|
||||
auth:
|
||||
# self_serve(默认):任何人都可注册,自动建空间 + Owner 成员
|
||||
# invite_only :禁止公开注册,新用户必须通过 /tenants/:id/members 邀请进入
|
||||
registration_mode: self_serve
|
||||
|
||||
audit:
|
||||
# 审计日志保留天数;每日后台清理;默认 90;置 0 关闭清理
|
||||
retention_days: 90
|
||||
```
|
||||
|
||||
环境变量(优先级高于 YAML):
|
||||
|
||||
| 环境变量 | YAML 路径 | 取值 |
|
||||
|----------|-----------|------|
|
||||
| `WEKNORA_TENANT_ENABLE_RBAC` | `tenant.enable_rbac` | `true` / `false` |
|
||||
| `WEKNORA_AUDIT_RETENTION_DAYS` | `audit.retention_days` | 非负整数 |
|
||||
|
||||
`auth.registration_mode` 没有专属环境变量,沿用历史的 `DISABLE_REGISTRATION=true`——一旦设置,启动时会把 `auth.registration_mode` 强制改成 `invite_only`,保证后端 API 和 `/auth/config` 驱动的前端注册入口一致。
|
||||
|
||||
启动日志会打印一行总结,确认本次启动到底使用了哪一组配置以及覆盖来源。
|
||||
|
||||
## 六、审计日志
|
||||
|
||||
`audit_logs` 表统一记录权限相关事件:
|
||||
|
||||
| Action | Outcome | 触发时机 |
|
||||
|--------|---------|----------|
|
||||
| `rbac.member_added` | success | `POST /tenants/:id/members` 成功 |
|
||||
| `rbac.member_removed` | success | `DELETE /tenants/:id/members/:user_id` 成功 |
|
||||
| `rbac.member_role_changed` | success | `PUT /tenants/:id/members/:user_id` 成功 |
|
||||
| `rbac.member_left` | success | `POST /tenants/:id/members/leave` 成功 |
|
||||
| `rbac.access_denied` | denied | `RequireRole` / `RequireOwnershipOrRole` 拒绝时(**仅 enforcement 开启时**) |
|
||||
|
||||
`access_denied` 采用 1 分钟滑动窗口去重,防止恶意探测刷表;同样的拒绝在应用日志(`[rbac] role insufficient ...`)里仍然条条可见。
|
||||
|
||||
知识库活动记录也复用这张不可变审计表,通过 `scope_type=knowledge_base`、`scope_id=<kb_id>` 建立范围索引,而不是为每个知识库创建独立日志表。`GET /api/v1/knowledge-bases/:id/activity` 提供游标分页,覆盖知识库配置、知识/FAQ、标签、数据源、共享、克隆/移动任务和 Wiki 内容变更;原有空间审计接口只返回无资源范围的 RBAC 事件,避免知识库活动淹没成员与拒绝记录。解析阶段明细继续由 knowledge spans 展示,数据源同步明细继续由 sync logs 展示,活动记录只保存摘要与关联 ID。
|
||||
|
||||
该接口仅知识库创建者或所属空间 Admin+ 可读,并明确拒绝从组织共享空间读取,避免向接收方泄露源空间操作者与配置历史。活动详情不得写入知识正文、外部 URL、数据源凭据或原始错误堆栈。
|
||||
|
||||
后台 goroutine `AuditLogRetentionRunner` 启动 ~10 分钟后开始首轮清理,之后每 24 小时清扫一次超过 `audit.retention_days` 的旧行;保留期为 `0` 时整条 goroutine 短路,不产生任何 DB 流量。
|
||||
|
||||
## 七、灰度上线建议
|
||||
|
||||
无论是自部署运维还是上游仓库本身,从「仅记录」切到「强制鉴权」都建议走以下流程:
|
||||
|
||||
1. **升级**:若想保留观察窗口,升级前先设置 `tenant.enable_rbac=false`(或环境变量)。否则默认就是强制鉴权——schema 落地、`tenant_members` 自动回填(每空间一个 Owner,其余 Contributor)、所有 KB 自动写入 `creator_id`。
|
||||
2. **核对成员**:调用 `GET /api/v1/tenants/:id/members` 确认:
|
||||
- 每个空间都只有一位 Owner;
|
||||
- Contributor / Viewer 划分符合预期;
|
||||
- 通过 `PUT /api/v1/tenants/:id/members/:user_id` / `DELETE` 调整。每次调整都会写入 `audit_logs`。
|
||||
3. **观察日志**:抓取应用日志里的 `[rbac] role insufficient (logged but not enforced) ...`,这些就是切换到强制鉴权后会变成 403 的请求。逐条修正成员角色或客户端身份。
|
||||
4. **切回强制鉴权**:删除 `tenant.enable_rbac=false` 覆盖(或显式置 `true`),重启服务。此后:
|
||||
- 角色不足 → 403;
|
||||
- 同时写入 `audit_logs.rbac.access_denied`(受去重控制)。
|
||||
5. **可选:禁用公开注册**:把 `auth.registration_mode` 改为 `invite_only`。登录页注册入口会自动消失,`POST /auth/register` 直接 403。
|
||||
|
||||
### 回滚
|
||||
|
||||
```bash
|
||||
export WEKNORA_TENANT_ENABLE_RBAC=false
|
||||
# 重启服务即可回到观察模式
|
||||
```
|
||||
|
||||
`tenant_members` 行与 `creator_id` 列保留,下次再启用无需重做回填。除非彻底放弃这个功能,否则**不要**回滚 `000043` / `000044` 迁移——`down.sql` 会丢弃 `tenant_members` 和 `audit_logs` 整张表。
|
||||
|
||||
## 八、前端表现
|
||||
|
||||
Pinia 中的 `authStore` 暴露:
|
||||
|
||||
- `authStore.currentTenantRole`:成员信息加载完成前为 `''`(loading 信号,按钮等待解析后再渲染,避免「先亮再灰」的闪烁);之后为四种角色之一。
|
||||
- `authStore.hasRole('admin')` 等:按层级判断的便捷函数。
|
||||
- 各资源页面再叠加 `isOwner`(如 `kb.creator_id === authStore.user?.id`)做 per-resource 判断。
|
||||
|
||||
这是后端守卫的镜像:**任何在后端会 403 的按钮,前端直接隐藏而不是让用户点了再吃错误。**
|
||||
|
||||
### 前端实际界面
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td colspan="2" align="center">
|
||||
<b>成员管理页</b><br/>
|
||||
<img src="./images/rbac-member-management.png" alt="成员管理" width="100%"/>
|
||||
<br/><sub>同时展示「待接受的邀请」和「空间成员」两组列表;只有 Owner 可以新增 / 移除成员;右上角的「审计日志」入口跳转到 <code>audit_logs</code> 视图。</sub>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%" align="center">
|
||||
<b>用户菜单 + 工作区切换器</b><br/>
|
||||
<img src="./images/rbac-workspace-switcher.png" alt="用户菜单 + 切换空间" width="100%"/>
|
||||
<br/><sub>左侧:当前空间角色徽章 / 设置入口 / 退出;右侧:切换到其它空间,「当前」角标标识活跃工作区。</sub>
|
||||
</td>
|
||||
<td width="50%" align="center">
|
||||
<b>自助创建工作区</b><br/>
|
||||
<img src="./images/rbac-create-workspace.png" alt="创建新空间" width="100%"/>
|
||||
<br/><sub>任何用户都可以自助创建空间,创建后自动成为新空间的 Owner(受 <code>WEKNORA_TENANT_MAX_PER_USER</code> 上限保护)。</sub>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" align="center">
|
||||
<b>待处理邀请弹窗</b><br/>
|
||||
<img src="./images/rbac-pending-invitation.png" alt="我的邀请" width="80%"/>
|
||||
<br/><sub>用户菜单上的邀请铃铛会展示来自其它空间的待处理邀请,可直接「接受 / 拒绝」;7 天未响应自动过期。</sub>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 九、常见问题
|
||||
|
||||
### 升级后所有人都变成了 Contributor,找不到 Admin?
|
||||
|
||||
回填逻辑选「每个空间里最早活跃的用户」作为 Owner,其余人统一变成 Contributor。如果创建空间的账号其实是一个机器人 / 共用账号,可能需要先用 `PUT /api/v1/tenants/:id/members/:user_id` 把机器人降级、把真人 Admin 提升。
|
||||
|
||||
### 切到强制鉴权后某个脚本开始 403?
|
||||
|
||||
大概率脚本的 JWT 对应的成员是 Viewer / Contributor 而非 Admin。两种解法:
|
||||
- 通过 `tenant_members` 把对应用户升级到 Admin;
|
||||
- 或者把脚本切到 `X-API-Key` 调用 —— API Key 在所属空间内固定 Admin。
|
||||
|
||||
### 共享空间里的成员为什么读不到我「以可写共享」过去的 KB?
|
||||
|
||||
请按第四节的判定顺序排查:
|
||||
- KB 是否真的属于源空间、`tenant_id` 配置是否正确;
|
||||
- 该共享关系当前是否还存在(未被取消);
|
||||
- 调用方在共享空间里是不是 Viewer;
|
||||
- 若 KB 的 `creator_id` 在源空间内为空且共享权限要求写,源空间的 RBAC 仍会要求 Admin+,导致跨空间写无法成立。
|
||||
|
||||
### 审计日志怎么有些 403 没记录?
|
||||
|
||||
两种可能:
|
||||
- 1 分钟滑动窗口去重,同一 `(actor, path, action)` 一分钟内只会写一行。完整序列在应用日志里。
|
||||
- `tenant.enable_rbac=false` 时仅记录成员管理事件,不写 `rbac.access_denied`。
|
||||
|
||||
### 我能不能做比「角色 + 归属」更细的 ACL?
|
||||
|
||||
v1 不支持。这个矩阵刻意保持成一个小固定格(Viewer < Contributor < Admin < Owner)+ 每种资源一个「creator escape hatch」。再细的策略(例如「Viewer 可以看自己的审计日志」)属于后续。
|
||||
|
||||
## 十、测试与可观测性
|
||||
|
||||
- `make test` 覆盖 `internal/middleware/rbac_test.go`、`internal/handler/rbac_lookups_test.go`、`internal/application/service/audit_log_test.go`、`internal/middleware/rbac_audit_test.go` 等约 25 个用例。
|
||||
- Langfuse / OpenTelemetry span 上携带解析后的 `TenantRole` 和 `TenantID`,一次被拒绝的请求在 trace 中即可看到对应角色,不需要再去手工关联日志。
|
||||
|
||||
## 相关文档
|
||||
|
||||
- 跨空间协作:[`共享空间说明.md`](./共享空间说明.md)
|
||||
- 多空间认证背景:[`OIDC认证调用流程.md`](./OIDC认证调用流程.md)
|
||||
- 配置项与环境变量:[`.env.example`](../.env.example)
|
||||
@@ -0,0 +1,46 @@
|
||||
# WeKnora Roadmap
|
||||
|
||||
本文档描述 WeKnora 的产品规划与计划方向,会随项目进展持续更新。
|
||||
|
||||
## 轻量化部署
|
||||
- [ ] WeKnora 官方提供原子化调用接口(Embedding、ReRank、LLM、文档解析等),并提供一定免费使用额度
|
||||
- [ ] WeKnora 官方提供完整云端服务,用户可在平台上直接体验 WeKnora 能力
|
||||
- [ ] 推出 WeKnora Lite 版本,供私有化部署需求不强的用户快速体验产品能力
|
||||
|
||||
## 知识理解
|
||||
- [x] 抽象整体文档解析模块,支持切换内置解析、MinerU 或其它解析方式
|
||||
- [ ] 优化文档分块策略,除规则分块外支持语义分块、章节分块等
|
||||
- [ ] 文档结构可视化:展示解析后的文档章节结构、图谱关系等
|
||||
- [ ] 支持音视频等更多文档格式,增强多模态理解能力
|
||||
|
||||
## 检索与总结
|
||||
- [ ] 支持在输入框中通过「@标签」指定检索范围
|
||||
- [ ] 支持在输入框中上传图片、附件进行检索
|
||||
- [x] 支持对话框中上传图片
|
||||
- [ ] 支持对话框中上传附件
|
||||
|
||||
## 知识库相关模型训练
|
||||
- [ ] 训练与检索召回相关的模型(Embedding、ReRank、LLM 等)
|
||||
- [ ] 在文档解析与文档理解方面持续探索,推进自研相关模型
|
||||
|
||||
## 知识库形态
|
||||
- [ ] 扩展知识库形态,支持时序数据的存储与索引
|
||||
- [ ] 探索知识库与 Memory 结合的应用场景
|
||||
|
||||
## IM 集成
|
||||
- [x] 支持与企微、飞书等 IM 系统集成,在 IM 内使用 WeKnora 能力
|
||||
|
||||
## 组件与扩展
|
||||
- [ ] 鼓励社区维护各厂商的模型服务、网络搜索服务等组件
|
||||
- [ ] 鼓励社区提供更多与知识库相关的 Skills
|
||||
|
||||
## 周边生态建设
|
||||
- [ ] 提供 Chrome 扩展,支持类似「剪藏」功能,将网页内容保存至知识库并支持检索、总结、问答
|
||||
- [ ] 提供小程序插件(具体形态待定)
|
||||
- [ ] 提供 JS SDK,便于在网页中集成 WeKnora 能力
|
||||
- [ ] 鼓励社区提供 VSCode、Cursor、Claude Code 等编辑器/IDE 插件
|
||||
|
||||
## 文档建设
|
||||
- [ ] 完善官方文档(使用说明、API、部署等)
|
||||
- [ ] 鼓励用户贡献文档、博客、视频等,形成社区化文档体系
|
||||
- [ ] 在知乎平台建设 WeKnora 内容合集
|
||||
@@ -0,0 +1,29 @@
|
||||
# UsageQuota migration
|
||||
|
||||
The native Go/Vue workbench now stores and manages per-member daily and monthly quotas at **用量分析 → 成员配额**. Migration `000202_usage_quotas` creates the quota table and the balance-hold lookup index. No legacy quota rows required importing: the legacy quota table was empty.
|
||||
|
||||
## Legacy behavior verified
|
||||
|
||||
- `server/app/models.py:743` defines user/organization scope, daily/monthly periods, token/amount/storage/concurrency limits, warning ratio, enabled state, and warn/soft/hard policies.
|
||||
- `server/app/main.py:7235` implements admin list, upsert, and delete; storage limits are only persisted.
|
||||
- `server/app/billing.py:276` computes quota state and contains a token-check helper, but `check_quota` has no callers in the application.
|
||||
- `server/app/training_reading.py:430` uses the minimum enabled positive concurrent limit, capped at three, for training reading parallelism.
|
||||
|
||||
## Native behavior
|
||||
|
||||
- Admin/owner-only APIs: `GET /api/v1/workbench/operations/quotas`, `PUT /api/v1/workbench/operations/quotas/:userID`, `DELETE /api/v1/workbench/operations/quotas/:id`. Handlers also enforce roles independently of route guards. All operations are tenant scoped; setting a quota requires membership in that tenant.
|
||||
- Daily and monthly periods use explicit `Asia/Shanghai` calendar boundaries, with half-open intervals `[start, end)`. Calls are attributed to the period in which their balance reservation starts. Calls crossing a boundary still count against the concurrent-call cap.
|
||||
- Token and point limits use completed usage plus active reservations. Zero means unlimited; disabled quotas neither block nor warn. Soft/hard policies both reject a proposed reservation that would exceed either limit. A reservation exactly equal to the remaining quota is permitted. Warn policy does not block these limits. The positive concurrent cap applies to all three policies.
|
||||
- Quota validation and balance reservation run in the same transaction under the member's existing balance-row lock. Failed checks roll back the balance reservation. Settlement remains idempotent and releases concurrent capacity; failed/cancelled model calls release their reservations through existing settlement.
|
||||
- Admin changes serialize with reservation checks. Both daily and monthly quotas apply simultaneously; configuration changes do not interrupt already-running calls.
|
||||
- Warnings appear in the admin table, and reaching the configured ratio with a reservation creates one member notification per quota period. The same account's parallel calls cannot create duplicate notices.
|
||||
- `points_limit` is **点数**, in the same units as the existing balance ledger. No currency price, exchange rate, or conversion from the unused legacy monetary field is invented. Actual provider usage above a reservation remains fully recorded and debited by the existing metering code.
|
||||
- Storage values remain explicitly marked as configuration only (`storage_enforced: false`), matching the old behavior. Auxiliary providers that do not expose token/billing information remain unpriced and outside chat balance reservation; the UI specifies that the concurrent cap covers balance-metered generation calls.
|
||||
|
||||
## Verification
|
||||
|
||||
- Targeted Go tests passed: quota policies, disabled/unlimited behavior, failed reservation rollback, settlement release, once-per-period warnings, period boundaries (including leap-year/month rollover), void usage exclusion, expired calls, user/tenant isolation, role restrictions, upsert/zero values, and validation. Existing balance/metering/cancellation tests passed alongside them.
|
||||
- Real PostgreSQL test `TestPostgresQuotaReservationRaces` passed in a fresh isolated `wb_quota_test_*` schema. Twelve simultaneous callers compete for token, point, and concurrent limits; exactly two succeed for each bounded case, and all twelve succeed when unlimited. Parallel duplicate settlements debit each call once and release all holds. The schema is dropped after the test.
|
||||
- Frontend `vue-tsc --build --pretty false` passed.
|
||||
- Live API/browser verifier: `scripts/verify_usage_quotas.cjs`. It creates one disposable member, tests admin CRUD, contributor/viewer denial, daily/monthly coexistence, warning and reservation totals, disabled behavior, UI edit/delete, and desktop/mobile rendering. It removes its quota/ledger fixtures and member and writes `acceptance/usage-quota-live-20260917.json` plus screenshots. It never invokes a paid external provider.
|
||||
- First live attempt confirmed quota write and role enforcement but detected a stale deployed binary lacking the already-tested GORM field exclusion. Its temporary fixtures were removed. Re-run the verifier after deploying the current source; the report records the outcome without hiding failures.
|
||||
@@ -0,0 +1,688 @@
|
||||
# Agent Skills 文档
|
||||
|
||||
## 概述
|
||||
|
||||
Agent Skills 是一种让 Agent 通过阅读"使用说明书"来学习新能力的扩展机制。与传统的硬编码工具不同,Skills 通过注入到 System Prompt 来扩展 Agent 的能力,遵循 **Progressive Disclosure(渐进式披露)** 的设计理念。
|
||||
目前仅支持带**智能推理**能力的智能体使用。前端可在智能体的编辑页面找到相关配置
|
||||
|
||||
### 核心特性
|
||||
|
||||
- **非侵入式扩展**:不影响原有 Agent ReAct 流程
|
||||
- **按需加载**:三级渐进式加载,优化 Token 使用
|
||||
- **沙箱执行**:脚本在隔离环境中安全执行
|
||||
- **灵活配置**:支持多目录、白名单过滤
|
||||
|
||||
## 设计理念
|
||||
|
||||
### Progressive Disclosure(渐进式披露)
|
||||
|
||||
Skills 采用三级加载机制,确保只在需要时才向 LLM 提供详细信息:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Level 1: 元数据 (Metadata) │
|
||||
│ • 始终加载到 System Prompt │
|
||||
│ • 约 100 tokens/skill │
|
||||
│ • 包含:技能名称 + 简短描述 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓ 用户请求匹配时
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Level 2: 指令 (Instructions) │
|
||||
│ • 通过 read_skill 工具按需加载 │
|
||||
│ • SKILL.md 的指令内容 │
|
||||
│ • 包含:详细指令、代码示例、使用方法 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓ 需要更多信息时
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Level 3: 附加资源 (Resources) │
|
||||
│ • 通过 read_skill 工具加载特定文件 │
|
||||
│ • 补充文档、配置模板、脚本文件 │
|
||||
│ • 通过 execute_skill_script 执行脚本 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Skill 目录结构
|
||||
|
||||
每个 Skill 是一个目录,包含 `SKILL.md` 主文件和可选的附加资源:
|
||||
|
||||
```
|
||||
my-skill/
|
||||
├── SKILL.md # 必需:主文件(含 YAML frontmatter)
|
||||
├── REFERENCE.md # 可选:补充文档
|
||||
├── templates/ # 可选:模板文件
|
||||
│ └── config.yaml
|
||||
└── scripts/ # 可选:可执行脚本
|
||||
├── analyze.py
|
||||
└── generate.sh
|
||||
```
|
||||
|
||||
## SKILL.md 格式
|
||||
|
||||
### YAML Frontmatter
|
||||
|
||||
每个 `SKILL.md` 必须以 YAML frontmatter 开头,定义元数据:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: pdf-processing
|
||||
description: Extract text and tables from PDF files, fill forms, merge documents. Use when working with PDF files or when the user mentions PDFs, forms, or document extraction.
|
||||
---
|
||||
|
||||
# PDF Processing
|
||||
|
||||
This skill provides utilities for working with PDF documents.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Use pdfplumber to extract text from PDFs:
|
||||
|
||||
```python
|
||||
import pdfplumber
|
||||
|
||||
with pdfplumber.open("document.pdf") as pdf:
|
||||
text = pdf.pages[0].extract_text()
|
||||
print(text)
|
||||
```
|
||||
|
||||
## 元数据验证规则
|
||||
|
||||
| 字段 | 要求 |
|
||||
|------|------|
|
||||
| `name` | 1-50 字符,仅允许汉字、英文字母、数字,不能是保留词 |
|
||||
| `description` | 1-500 字符,描述技能用途和触发条件 |
|
||||
|
||||
**保留词**:`system`, `default`, `internal`, `core`, `base`, `root`, `admin`
|
||||
|
||||
|
||||
## 配置
|
||||
|
||||
### AgentConfig 配置项
|
||||
|
||||
```go
|
||||
type AgentConfig struct {
|
||||
// ... 其他配置 ...
|
||||
|
||||
// Skills 相关配置
|
||||
SkillsEnabled bool `json:"skills_enabled"` // 是否启用 Skills
|
||||
SkillDirs []string `json:"skill_dirs"` // Skill 目录列表
|
||||
AllowedSkills []string `json:"allowed_skills"` // 白名单(空=全部允许)
|
||||
}
|
||||
```
|
||||
|
||||
### 配置示例
|
||||
|
||||
```json
|
||||
{
|
||||
"skills_enabled": true,
|
||||
"skill_dirs": [
|
||||
"/path/to/project/skills",
|
||||
"/home/user/.agent-skills"
|
||||
],
|
||||
"allowed_skills": ["pdf-processing", "code-review"]
|
||||
}
|
||||
```
|
||||
|
||||
### Sandbox 配置入口
|
||||
|
||||
Sandbox 不再把凭据和模板放进 `WEKNORA_SANDBOX_*`。后端、凭据、模板、执行超时、TTL 和私网访问策略均在「设置 → 沙箱后端」按空间保存;智能体没有选择空间配置时,脚本执行保持禁用。Docker 后端默认关闭,由系统管理员在「设置 → 系统设置」打开,或设置 `WEKNORA_SANDBOX_DOCKER_ENABLED=true`。
|
||||
|
||||
### Sandbox 模式
|
||||
|
||||
Docker、CubeSandbox、E2B 均通过同一套空间配置 CRUD、连接检查和智能体选择接口管理。CubeSandbox / E2B 的集群搭建和设置页接入流程见 [WeKnora 沙箱集群与标准模板](sandbox-cluster.md)。设置页会通过当前连接拉取模板目录;若没有 WeKnora 标准模板,后端会从标准镜像发起创建,用户无需复制模板 ID。
|
||||
|
||||
| 模式 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| `docker` | 稳定 | 单机 Docker daemon;会话级持久(一个会话一个长驻容器),支持多机 WeKnora 副本(需 Redis),但沙箱都落在同一台 daemon 上。见 [Docker 沙箱后端](sandbox-docker-backend.md) |
|
||||
| `cube` | 稳定 | Tencent CubeSandbox MicroVM;会话级持久,支持多机(需 Redis) |
|
||||
| `e2b` | 稳定 | E2B 云端 MicroVM;会话级持久,支持多机(需 Redis);依赖第三方 SDK go-e2b |
|
||||
|
||||
### 工作区沙箱后端配置
|
||||
|
||||
一个工作区可以维护**多份具名**沙箱后端配置(「设置 → 沙箱后端」),智能体在编辑弹窗的「能力扩展 → 沙箱后端」里各自选一份。不选表示禁用脚本执行。
|
||||
|
||||
同一后端类型可以有多份配置:例如两份 E2B 分别指向不同账号或区域,让不同智能体的技能脚本落在不同配额上。
|
||||
|
||||
**空间配置是唯一运行时来源。** 端点、凭据和运行参数不会从 `.env` 回退:
|
||||
|
||||
| 后端 | 必填 | 可留空 |
|
||||
|---|---|---|
|
||||
| Cube | API 端点、Proxy 端点、沙箱域名、从集群列表选择的模板 | API Key(自建部署通常无鉴权) |
|
||||
| E2B | API Key、从账号列表选择的模板 | API 端点、沙箱域名(go-e2b 自行解析默认值) |
|
||||
|
||||
留空必填项在保存时就会被拒绝(HTTP 400)。HTTP 超时、沙箱 TTL 和执行超时留空均使用程序内置默认值。
|
||||
|
||||
这条规则换来的是:库里那一行就是沙箱位置的完整描述。因此身份比较不必再去解析 `.env`,改 `.env` 也不会在无人察觉的情况下把某份配置重新指向别的账号。
|
||||
|
||||
**会话与配置的绑定是「随沙箱同生共死」的钉子。** 会话首次创建沙箱时,把当时用的配置 ID 记在 `sessions.sandbox_config_id` 上;此后该会话的附件上传、产物收集、沙箱销毁都锁定在这份配置上。改智能体的选择**只影响之后新建的沙箱**——否则管理员改一次配置,正在进行的会话就会去错误的账号里找产物,销毁也会打空,留下一个没人知道 ID 的 paused 沙箱持续计费。
|
||||
|
||||
### 安装租户技能
|
||||
|
||||
空间「技能沙箱」设置里可以把技能装进当前配置的镜像。除上传 zip 外,也支持从托管平台粘贴来源(每种写法只对应一种来源,不会猜测):
|
||||
|
||||
- ClawHub:`@owner/slug`,或不含 `/` 的 slug(如 `my-team--skill`)
|
||||
- ClawHub skills.sh 联邦页:`https://clawhub.ai/skills-sh/owner/repo/slug` 或 `skills-sh:owner/repo/slug`(服务端向 ClawHub 解析钉死的 GitHub commit,不会把 URL 最后一段当成仓库子目录)
|
||||
- 页面链接:ClawHub / [skillhub.cn](https://skillhub.cn) / 自托管 SkillHub、skills.sh、GitHub、GitLab
|
||||
- 直接的 zip / `SKILL.md` URL
|
||||
|
||||
不要粘贴裸的 `owner/slug`:请改成 `@owner/slug` 或完整 `https://github.com/...` 链接。来源必须可匿名读取,服务端下载时不携带任何凭据;私有仓库请先导出 zip 再上传。安装仍走原有镜像快照流程。
|
||||
|
||||
**有沙箱在跑时改不了身份字段。** 身份字段分两组,成因不同但后果都足够严重:
|
||||
|
||||
| 组 | 字段 | 一改会怎样 |
|
||||
|---|---|---|
|
||||
| 控制面 | 后端类型、API 端点、API Key | 旧沙箱**再也无法列举/删除/恢复**——新凭据没有权限动它们,而 `onTimeout=pause` 意味着 TTL 也不会回收|
|
||||
| 数据面 | E2B 沙箱域名;Cube 代理地址、沙箱域名 | 旧沙箱仍可删,但 envd 请求会打到错的主机 ⇒ 该配置下**所有活会话立刻失效**,而沙箱还活着继续计费 |
|
||||
|
||||
因此这类修改会被拒绝(HTTP 409),界面会给出沙箱数量、受影响会话数,以及两条出路:**结束或删除那些会话**(删会话会销毁其沙箱),或者**新建一份配置**把智能体指过去(旧凭据原样留着,清理能力不丢)。**没有「释放沙箱」按钮**——那等于在管理员背后销毁正在进行的对话。
|
||||
|
||||
**删除配置**只拦远端沙箱,不拦智能体引用:确认弹窗会列出仍指向它的智能体名单,但不阻止删除;删除后那些智能体执行技能时会明确报错,而不是静默换到别的后端。若后端已连不上、无法核实是否仍有沙箱,可以强制删除(这是唯一能强制的情形——能数出来的活沙箱永远不让强删)。
|
||||
|
||||
**`sessions.sandbox_config_id` 取值语义:**
|
||||
|
||||
| 值 | 含义 |
|
||||
|---|---|
|
||||
| `NULL` | 当前无活沙箱(删会话 / 销毁后会 Clear) |
|
||||
| `"-"` | 旧版本部署默认沙箱的历史兼容标记;新会话不再写入 |
|
||||
| UUID | 活沙箱建在工作区某份**具名配置**上 |
|
||||
|
||||
|
||||
### 会话级 sandbox 部署要点
|
||||
|
||||
- **binding store 自动选择**:进程根据通用 `REDIS_ADDR` 是否配置自动决定绑定存储;Redis key 命名空间复用 `WEKNORA_REDIS_NAMESPACE`,未设置时为 `weknora`。
|
||||
- **多机部署(生产推荐)**:配置 `REDIS_ADDR`。多副本共享同一 session 的沙箱绑定,通过 Redis SET NX + 可续租分布式锁串行化 create / recover / delete。
|
||||
- **⚠️ binding store 现在保存承载凭证,必须做访问控制**:入站一律要求凭证,Cube / E2B 的 traffic token 随绑定一起明文存放——它是访问该沙箱公网 URL 的凭证,读到它就等于能访问那个沙箱暴露的端口。因此**存放绑定的 Redis 必须启用认证并限制网络可达范围**,不能与不受信任的服务共用实例。这里不加密是有意的:每次重连都要解密会把开销加在热路径上,而 Redis 本身的访问控制是更合适的边界。代码侧的对应约束是**绝不整体打印绑定或 handle**(日志里只出现 `sandbox_id` 与凭证的有无),改动 `session_binding.go` / 适配器日志时请一并保持。
|
||||
- **单机部署**:不配置 `REDIS_ADDR`(或 Lite 模式)时使用进程内内存 binding,仅限单实例。进程重启会丢失 session→sandbox 映射,remote 侧沙箱成为孤儿(注意:**TTL 到期只会暂停、不会销毁**,见下)。绑定丢失后,**Cube / E2B 配置不会再按 metadata 领养旧沙箱**——旧沙箱的 traffic token 随绑定一起没了且 provider 不会重发,领养只会得到一个每次数据面调用都 403 的死会话。lifecycle 会直接删掉它再新建,`/workspace` 里的临时文件随之丢失。Docker 没有 traffic token 概念,照旧领养。
|
||||
- **切换 provider**:不同 provider 的 sandbox ID 不通用。智能体改选配置只影响之后新建的沙箱,已有沙箱继续按 session pin 回收。
|
||||
- **⚠️ 孤儿沙箱不会被 TTL 自动回收**:会话沙箱创建时使用 `onTimeout=pause` + `autoResume=true`(见 `buildSessionCreateRequest`),因此 **TTL 到期是"暂停"而非"销毁"**——保留状态本就是 pause 的目的。加上 CAS 换绑会把旧 sandboxID 从 binding store 覆盖掉,被替换的沙箱会变成**无人知晓 ID 的 paused 孤儿**,持续占用快照存储与费用。删除会话(`session.go` 的 destroyer)与 lifecycle 的惰性 orphan cleanup 都覆盖不到这种情况。生产环境需依赖按 metadata 列举并与 binding 对账的清理任务来回收(`internal/sandbox/orphan_reaper.go`),且**必须显式包含 `paused` 状态**。对账维度是 `(tenant_id, config_id)` 而非仅 `tenant_id`:同一工作区的两份配置可能指向**同一个 provider 账号**(例如同一个 E2B Key 只差模板),只按 `tenant_id` 过滤会把另一份配置的沙箱一并误删。
|
||||
- **网络策略**:每份具名沙箱配置带一块 `network` 策略,作用于该配置下所有沙箱(会话、技能安装、深度检查共用同一份)。默认**出站放行**;**入站一律要求凭证**——沙箱公网 URL 必须携带创建时签发的 traffic token,WeKnora 自身的 envd 链路会自动携带(Cube 由 SDK 附加,E2B 由 WeKnora 的数据面 transport 附加)。若 Cube / E2B 没有签发 token,create 会失败并销毁该沙箱,而不是把空凭证写入 binding(数据面 403 会被当成 authentication,会话会永久卡住)。管理员可配置 allow / deny 列表,Cube 额外支持 CubeEgress L7 规则(scheme / sni / host / method / path + 审计 + header 注入),E2B 额外支持按 host 注入 header。Docker 只能整体开关(`bridge` / `none`)。表单不再提供入站开关;解析忽略已存的 `allow_public_inbound`,保存时清掉该字段。**改策略只影响之后新建的沙箱**:本期不做运行中热更新。
|
||||
- **⚠️ 升级行为变更**:入站从「公网可达」改为「一律要求凭证」。浏览器或外部服务不能再直连沙箱端口;管理界面和 API 都打不开入站。
|
||||
|
||||
## Agent 工具
|
||||
|
||||
Skills 功能通过两个工具与 Agent 交互:
|
||||
|
||||
### read_skill
|
||||
|
||||
读取技能内容或特定文件。
|
||||
|
||||
**参数**:
|
||||
```json
|
||||
{
|
||||
"skill_name": "pdf-processing", // 必需:技能名称
|
||||
"file_path": "FORMS.md" // 可选:相对路径
|
||||
}
|
||||
```
|
||||
|
||||
**使用场景**:
|
||||
1. 加载 Level 2 内容:仅传 `skill_name`
|
||||
2. 加载 Level 3 资源:同时传 `skill_name` 和 `file_path`
|
||||
|
||||
**示例调用**:
|
||||
```json
|
||||
// 加载技能主内容
|
||||
{"skill_name": "pdf-processing"}
|
||||
|
||||
// 加载补充文档
|
||||
{"skill_name": "pdf-processing", "file_path": "FORMS.md"}
|
||||
|
||||
// 查看脚本内容
|
||||
{"skill_name": "pdf-processing", "file_path": "scripts/analyze.py"}
|
||||
```
|
||||
|
||||
### execute_skill_script
|
||||
|
||||
在沙箱中执行技能脚本。
|
||||
|
||||
**参数**:
|
||||
```json
|
||||
{
|
||||
"skill_name": "pdf-processing", // 必需:技能名称
|
||||
"script_path": "scripts/analyze.py", // 必需:脚本相对路径
|
||||
"args": ["input.pdf", "--format", "json"] // 可选:命令行参数
|
||||
}
|
||||
```
|
||||
|
||||
**支持的脚本类型**:
|
||||
- Python (`.py`)
|
||||
- Shell (`.sh`)
|
||||
- JavaScript/Node.js (`.js`)
|
||||
- Ruby (`.rb`)
|
||||
- Go (`.go`)
|
||||
|
||||
## 预加载技能(Preloaded Skills)
|
||||
|
||||
系统内置了以下 5 个预加载技能,用于增强知识库问答和文档处理能力:
|
||||
|
||||
### 1. citation-generator - 引用生成器
|
||||
|
||||
**用途**:自动生成规范引用格式
|
||||
|
||||
**触发场景**:
|
||||
- 需要生成参考文献
|
||||
- 标注知识库内容出处
|
||||
- 要求提供引用信息
|
||||
|
||||
**核心能力**:
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| 来源标注 | 为回答中使用的每个知识点标注来源 |
|
||||
| 格式化引用 | 支持 APA、MLA、Chicago、简化格式 |
|
||||
| 参考文献列表 | 在回答末尾生成完整的参考文献列表 |
|
||||
|
||||
**简化引用格式示例**:
|
||||
```
|
||||
根据公司政策[员工手册2024.pdf, 第15页],年假申请需提前...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. data-processor - 数据处理器
|
||||
|
||||
**用途**:数据处理与分析
|
||||
|
||||
**触发场景**:
|
||||
- "分析这些数据"、"统计一下"、"计算总数/平均值"
|
||||
- "转换为 JSON/CSV 格式"
|
||||
- "提取关键信息"、"整理成表格"
|
||||
- "生成报告"、"数据汇总"
|
||||
|
||||
**核心能力**:
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| 数据分析 | 对检索到的文档数据进行统计分析 |
|
||||
| 格式转换 | JSON/CSV/Markdown 等格式相互转换 |
|
||||
| 数据提取 | 从非结构化文本中提取结构化信息 |
|
||||
| 报告生成 | 生成数据分析报告和摘要 |
|
||||
|
||||
**可用脚本**:
|
||||
- `scripts/analyze.py` - 数据分析脚本
|
||||
- `scripts/format_converter.py` - 格式转换脚本
|
||||
- `scripts/extract_info.py` - 信息提取脚本
|
||||
|
||||
**脚本使用示例**:
|
||||
```bash
|
||||
# 数据分析
|
||||
echo '{"items": [1, 2, 3, 4, 5]}' | python scripts/analyze.py
|
||||
|
||||
# 格式转换(JSON 转 CSV)
|
||||
echo '[{"name": "A", "value": 1}]' | python scripts/format_converter.py --to csv
|
||||
|
||||
# 信息提取
|
||||
echo "2024年销售额为100万元" | python scripts/extract_info.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. doc-coauthoring - 文档协作 (源于Claude官方Skill)
|
||||
|
||||
**用途**:引导用户完成结构化文档创作
|
||||
|
||||
**触发场景**:
|
||||
- 编写文档:"write a doc"、"draft a proposal"、"create a spec"
|
||||
- 文档类型:PRD、设计文档、决策文档、RFC
|
||||
|
||||
**工作流程**:
|
||||
|
||||
```
|
||||
Stage 1: 上下文收集 (Context Gathering)
|
||||
↓
|
||||
Stage 2: 细化与结构 (Refinement & Structure)
|
||||
↓
|
||||
Stage 3: 读者测试 (Reader Testing)
|
||||
```
|
||||
|
||||
**三阶段说明**:
|
||||
| 阶段 | 目标 | 关键活动 |
|
||||
|------|------|----------|
|
||||
| Stage 1 | 缩小用户与 Claude 之间的信息差 | 元信息提问、上下文收集、澄清问题 |
|
||||
| Stage 2 | 逐节构建文档 | 头脑风暴、筛选整理、迭代修改 |
|
||||
| Stage 3 | 测试文档对读者的效果 | 预测读者问题、子代理测试、修复盲点 |
|
||||
|
||||
---
|
||||
|
||||
### 4. document-analyzer - 文档分析器
|
||||
|
||||
**用途**:深度分析文档结构和内容
|
||||
|
||||
**触发场景**:
|
||||
- 分析文档结构
|
||||
- 提取关键信息
|
||||
- 识别文档类型
|
||||
- 进行内容质量评估
|
||||
|
||||
**核心能力**:
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| 结构分析 | 识别文档的章节层级、组织架构 |
|
||||
| 关键信息提取 | 提取核心论点、关键数据、重要结论 |
|
||||
| 文档类型识别 | 判断文档类型(报告、手册、论文、合同等) |
|
||||
| 内容质量评估 | 评估文档的完整性、一致性、可读性 |
|
||||
|
||||
**分析流程**:
|
||||
1. **文档概览** - 获取文档基本信息
|
||||
2. **结构分析** - 识别标题层级、章节组织
|
||||
3. **内容提取** - 提取核心主题、关键论点、支撑数据
|
||||
4. **质量评估** - 评估完整性、一致性、清晰度
|
||||
|
||||
---
|
||||
|
||||
### 技能目录结构
|
||||
|
||||
预加载技能位于 `skills/preloaded/` 目录下:
|
||||
|
||||
```
|
||||
skills/preloaded/
|
||||
├── citation-generator/
|
||||
│ └── SKILL.md
|
||||
├── data-processor/
|
||||
│ ├── SKILL.md
|
||||
│ └── scripts/
|
||||
│ ├── analyze.py
|
||||
│ ├── format_converter.py
|
||||
│ └── extract_info.py
|
||||
├── doc-coauthoring/
|
||||
│ └── SKILL.md
|
||||
├── document-analyzer/
|
||||
│ └── SKILL.md
|
||||
└── summary-generator/
|
||||
└── SKILL.md
|
||||
```
|
||||
|
||||
## 创建自定义 Skill
|
||||
|
||||
暂时不支持用户自主创建自定义 Skill
|
||||
|
||||
|
||||
## 沙箱安全机制
|
||||
|
||||
### 脚本安全校验(Script Validator)
|
||||
|
||||
在脚本执行前,系统会进行多层安全校验,拦截潜在的恶意操作:
|
||||
|
||||
#### 校验类型
|
||||
|
||||
| 类型 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| **危险命令检测** | 检测可能破坏系统的命令 | `rm -rf /`, `mkfs`, `shutdown`, fork bombs |
|
||||
| **危险模式匹配** | 正则匹配高危操作模式 | `curl \| bash`, `base64 -d`, `eval()` |
|
||||
| **网络访问检测** | 检测网络请求尝试 | `curl`, `wget`, `socket.connect`, `requests.get` |
|
||||
| **反向 Shell 检测** | 检测远程控制后门 | `/dev/tcp/`, `bash -i`, `nc -e` |
|
||||
| **参数注入检测** | 检测命令行参数中的注入 | `&&`, `\|`, `$()`, 反引号 |
|
||||
| **Stdin 注入检测** | 检测标准输入中的嵌入命令 | 嵌入的命令替换语法 |
|
||||
|
||||
#### 拦截的危险命令
|
||||
|
||||
**系统破坏类**:
|
||||
- `rm -rf /`, `rm -rf /*` - 递归删除根目录
|
||||
- `mkfs`, `dd if=/dev/zero` - 文件系统/磁盘操作
|
||||
- Fork bombs: `:(){ :|:& };:`
|
||||
|
||||
**系统控制类**:
|
||||
- `shutdown`, `reboot`, `halt`, `poweroff`
|
||||
- `killall`, `pkill`
|
||||
- `systemctl`, `service`
|
||||
|
||||
**权限提升类**:
|
||||
- `chmod 777 /`, `chown root`
|
||||
- `setuid`, `setgid`, `passwd`
|
||||
- 访问 `/etc/passwd`, `/etc/shadow`, `/etc/sudoers`
|
||||
|
||||
**凭证窃取类**:
|
||||
- 访问 `.ssh/`, `id_rsa`, `id_ed25519`
|
||||
- 读取敏感配置文件
|
||||
|
||||
**容器逃逸类**:
|
||||
- `docker`, `kubectl`, `nsenter`
|
||||
- `unshare`, `capsh`
|
||||
|
||||
#### 拦截的危险模式
|
||||
|
||||
**代码注入**:
|
||||
```
|
||||
# 以下模式会被拦截
|
||||
curl ... | bash # 下载并执行
|
||||
wget ... | sh # 下载并执行
|
||||
eval() # 动态代码执行
|
||||
exec() # 命令执行
|
||||
os.system() # 系统命令执行
|
||||
subprocess.Popen(shell=True) # Shell 命令执行
|
||||
```
|
||||
|
||||
**编码绕过尝试**:
|
||||
```
|
||||
# 以下模式会被拦截
|
||||
base64 -d # Base64 解码执行
|
||||
echo ... | base64 -d # 管道解码
|
||||
xxd -r # Hex 解码
|
||||
```
|
||||
|
||||
**Python 特有风险**:
|
||||
```python
|
||||
# 以下模式会被拦截
|
||||
__import__() # 动态导入
|
||||
pickle.load() # 反序列化(可执行任意代码)
|
||||
yaml.load() # 不安全的 YAML 加载
|
||||
yaml.unsafe_load() # 显式不安全加载
|
||||
```
|
||||
|
||||
#### Shell 操作符拦截
|
||||
|
||||
参数中包含以下操作符时会被拦截:
|
||||
|
||||
| 操作符 | 说明 |
|
||||
|--------|------|
|
||||
| `&&`, `\|\|` | 命令链接 |
|
||||
| `;` | 命令分隔 |
|
||||
| `\|` | 管道 |
|
||||
| `$()`, `` ` `` | 命令替换 |
|
||||
| `>`, `>>`, `<` | 重定向 |
|
||||
| `2>`, `&>` | 错误/组合重定向 |
|
||||
| `\n`, `\r` | 换行注入 |
|
||||
|
||||
#### 校验结果
|
||||
|
||||
校验失败时返回详细的错误信息:
|
||||
|
||||
```go
|
||||
type ValidationError struct {
|
||||
Type string // 错误类型:dangerous_command, dangerous_pattern, arg_injection 等
|
||||
Pattern string // 匹配到的模式
|
||||
Context string // 上下文信息
|
||||
Message string // 人类可读的描述
|
||||
}
|
||||
```
|
||||
|
||||
**示例错误**:
|
||||
```
|
||||
security validation failed [dangerous_command]: Script contains dangerous command: rm -rf / (pattern: rm -rf /, context: ...cleanup && rm -rf / && echo done...)
|
||||
```
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```go
|
||||
// 创建校验器
|
||||
validator := sandbox.NewScriptValidator()
|
||||
|
||||
// 校验脚本内容
|
||||
result := validator.ValidateScript(scriptContent)
|
||||
if !result.Valid {
|
||||
for _, err := range result.Errors {
|
||||
log.Printf("Security error: %s", err.Error())
|
||||
}
|
||||
return errors.New("script validation failed")
|
||||
}
|
||||
|
||||
// 校验命令行参数
|
||||
argsResult := validator.ValidateArgs(args)
|
||||
|
||||
// 校验标准输入
|
||||
stdinResult := validator.ValidateStdin(stdin)
|
||||
|
||||
// 或一次性校验全部
|
||||
fullResult := validator.ValidateAll(scriptContent, args, stdin)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Docker 沙箱
|
||||
|
||||
Docker 模式提供最强的隔离:
|
||||
|
||||
- **非 root 用户**:容器内以普通用户运行
|
||||
- **Capability 限制**:移除所有 Linux capabilities
|
||||
- **只读文件系统**:根文件系统只读
|
||||
- **资源限制**:内存 256MB,CPU 限制
|
||||
- **网络隔离**:默认无网络访问
|
||||
- **临时挂载**:Skill 目录只读挂载
|
||||
- **脚本预校验**:执行前进行安全校验
|
||||
|
||||
#### 沙箱镜像
|
||||
|
||||
系统使用专用的沙箱镜像 `wechatopenai/weknora-sandbox`,预装了 Python 3.11、Node.js 20、常用 CLI 工具和 Python 库,无需在执行时临时安装依赖。
|
||||
|
||||
**预拉取镜像**(推荐在首次部署时执行,避免首次执行脚本时等待下载):
|
||||
|
||||
```bash
|
||||
# 方式一:直接拉取
|
||||
docker pull wechatopenai/weknora-sandbox:main
|
||||
|
||||
# 方式二:本地构建
|
||||
sh scripts/build_images.sh -s
|
||||
```
|
||||
|
||||
> 如果未预拉取,创建第一个沙箱时会先拉取镜像,首次执行需要等待下载完成;也可以在设置页的模板步骤提前触发拉取。
|
||||
|
||||
> 用 `main` 而非 `latest`:`latest` 只在发版时移动,目前仍停在 `/workspace` 及其 `input`/`output` 目录交给沙箱账号之前的版本,用它建出来的沙箱写不了自己的产物目录。发版带上该修复后即可换回 `latest`。
|
||||
|
||||
**镜像内置环境**:
|
||||
- Python 3.11 + pip(requests、pyyaml、pandas、beautifulsoup4)
|
||||
- Node.js 20 + npm
|
||||
- CLI 工具:jq、curl、bash、grep、sed、awk 等
|
||||
|
||||
```bash
|
||||
# Docker 执行示例
|
||||
docker run --rm \
|
||||
--user 1000:1000 \
|
||||
--cap-drop ALL \
|
||||
--read-only \
|
||||
--memory=256m \
|
||||
--network=none \
|
||||
-v /path/to/skill:/skill:ro \
|
||||
-w /skill \
|
||||
wechatopenai/weknora-sandbox:main \
|
||||
python scripts/analyze.py input.pdf
|
||||
```
|
||||
|
||||
## API 参考
|
||||
|
||||
### SkillManager
|
||||
|
||||
```go
|
||||
type Manager interface {
|
||||
// 初始化,发现所有 Skills
|
||||
Initialize(ctx context.Context) error
|
||||
|
||||
// 获取所有 Skill 元数据(Level 1)
|
||||
GetAllMetadata() []*SkillMetadata
|
||||
|
||||
// 加载 Skill 指令(Level 2)
|
||||
LoadSkill(ctx context.Context, skillName string) (*Skill, error)
|
||||
|
||||
// 读取 Skill 文件内容(Level 3)
|
||||
ReadSkillFile(ctx context.Context, skillName, filePath string) (string, error)
|
||||
|
||||
// 列出 Skill 中的所有文件
|
||||
ListSkillFiles(ctx context.Context, skillName string) ([]string, error)
|
||||
|
||||
// 执行 Skill 脚本
|
||||
ExecuteScript(ctx context.Context, skillName, scriptPath string, args []string) (*sandbox.ExecuteResult, error)
|
||||
|
||||
// 检查是否启用
|
||||
IsEnabled() bool
|
||||
}
|
||||
```
|
||||
|
||||
### Skill 结构
|
||||
|
||||
```go
|
||||
type Skill struct {
|
||||
Name string // 技能名称
|
||||
Description string // 技能描述
|
||||
BasePath string // 目录绝对路径
|
||||
FilePath string // SKILL.md 绝对路径
|
||||
Instructions string // SKILL.md 主体指令内容
|
||||
Loaded bool // 是否已加载 Level 2
|
||||
}
|
||||
|
||||
type SkillMetadata struct {
|
||||
Name string // 技能名称
|
||||
Description string // 技能描述
|
||||
BasePath string // 目录路径
|
||||
}
|
||||
```
|
||||
|
||||
### ExecuteResult 结构
|
||||
|
||||
```go
|
||||
type ExecuteResult struct {
|
||||
ExitCode int // 退出码
|
||||
Stdout string // 标准输出
|
||||
Stderr string // 标准错误
|
||||
Duration time.Duration // 执行时长
|
||||
Error error // 执行错误
|
||||
}
|
||||
```
|
||||
|
||||
## 示例:完整工作流
|
||||
|
||||
以下是 Agent 处理用户请求的完整流程:
|
||||
|
||||
```
|
||||
用户: "帮我从 report.pdf 提取表格数据"
|
||||
|
||||
Agent 思考:
|
||||
→ 查看 System Prompt 中的 Skills 列表
|
||||
→ 发现 "pdf-processing" 技能匹配
|
||||
|
||||
Agent 行动 1: 调用 read_skill
|
||||
→ {"skill_name": "pdf-processing"}
|
||||
→ 获取 SKILL.md 指令内容
|
||||
→ 学习如何使用 pdfplumber
|
||||
|
||||
Agent 行动 2: 调用 execute_skill_script
|
||||
→ {"skill_name": "pdf-processing",
|
||||
"script_path": "scripts/extract_text.py",
|
||||
"args": ["report.pdf"]}
|
||||
→ 脚本在沙箱中执行,返回提取的表格数据
|
||||
|
||||
Agent 回复:
|
||||
→ 向用户展示提取的表格数据
|
||||
→ 提供数据使用建议
|
||||
```
|
||||
|
||||
## 故障排查
|
||||
|
||||
### Skill 未被发现
|
||||
|
||||
1. 检查 `skill_dirs` 配置是否正确
|
||||
2. 确认目录中存在 `SKILL.md` 文件
|
||||
3. 验证 YAML frontmatter 格式
|
||||
|
||||
```bash
|
||||
# 运行 demo 验证
|
||||
go run ./cmd/skills-demo/main.go
|
||||
```
|
||||
|
||||
### 脚本执行失败
|
||||
|
||||
1. 检查沙箱后端配置
|
||||
2. Docker 模式:确认 Docker 服务运行中
|
||||
3. 检查脚本权限和语法
|
||||
|
||||
### 元数据验证错误
|
||||
|
||||
常见错误:
|
||||
- `skill name too long`: 名称超过 50 字符
|
||||
- `skill name contains invalid characters`: 包含非法字符
|
||||
- `skill name is reserved`: 使用了保留词
|
||||
- `skill description too long`: 描述超过 500 字符
|
||||
@@ -0,0 +1,140 @@
|
||||
# WeKnora API 文档
|
||||
|
||||
## 目录
|
||||
|
||||
- [概述](#概述)
|
||||
- [最权威参考:Swagger UI](#最权威参考swagger-ui)
|
||||
- [基础信息](#基础信息)
|
||||
- [认证机制](#认证机制)
|
||||
- [错误处理](#错误处理)
|
||||
- [文件与图片引用(`resource://` 与直链)](#文件与图片引用resource-与直链)
|
||||
- [API 概览](#api-概览)
|
||||
|
||||
## 概述
|
||||
|
||||
WeKnora 提供了一系列 RESTful API,用于创建和管理知识库、检索知识,以及进行基于知识的问答。本文档详细描述了这些 API 的使用方式。
|
||||
|
||||
## 最权威参考:Swagger UI
|
||||
|
||||
WeKnora 同时提供基于 OpenAPI 的 Swagger 文档。**启动服务后访问 `http://localhost:8080/swagger/index.html`**,可看到所有端点的完整参数、请求/响应 schema,并可直接在浏览器内试调——它随代码自动更新,是最准确的接口参考。
|
||||
|
||||
本目录下的 markdown 文档提供更易读的示例与场景说明,与 swagger 同步维护;当二者出现差异时,以 swagger 为准。
|
||||
|
||||
> Swagger UI 仅在非 release 模式(`GIN_MODE != release`)下挂载;生产部署默认关闭。
|
||||
|
||||
## 基础信息
|
||||
|
||||
- **基础 URL**: `/api/v1`
|
||||
- **响应格式**: JSON
|
||||
- **认证方式**: API Key
|
||||
|
||||
## 认证机制
|
||||
|
||||
所有 API 请求需要在 HTTP 请求头中包含 `X-API-Key` 进行身份认证:
|
||||
|
||||
```
|
||||
X-API-Key: your_api_key
|
||||
```
|
||||
|
||||
为便于问题追踪和调试,建议每个请求的 HTTP 请求头中添加 `X-Request-ID`:
|
||||
|
||||
```
|
||||
X-Request-ID: unique_request_id
|
||||
```
|
||||
|
||||
### 获取 API Key
|
||||
|
||||
在 web 页面完成账户注册后,请前往账户信息页面获取您的 API Key。
|
||||
|
||||
请妥善保管您的 API Key,避免泄露。API Key 代表您的账户身份,拥有完整的 API 访问权限。
|
||||
|
||||
## 错误处理
|
||||
|
||||
所有 API 使用标准的 HTTP 状态码表示请求状态,并返回统一的错误响应格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "错误代码",
|
||||
"message": "错误信息",
|
||||
"details": "错误详情"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 文件与图片引用(`resource://` 与直链)
|
||||
|
||||
响应里的图片、图表、附件默认以内部引用 `resource://<handle>` 返回,例如问答答案中的
|
||||
``。这类引用不能被浏览器直接加载,客户端需要再
|
||||
调用带鉴权的 `GET /files?file_path=<引用>` 代理去取字节流。
|
||||
|
||||
如果你在把 WeKnora 集成进自己的 App,可以让服务端直接返回**可加载的 http(s) 直链**,省掉这一次
|
||||
额外请求:
|
||||
|
||||
| 方式 | 用法 | 生效范围 |
|
||||
|------|------|----------|
|
||||
| 单次请求 | 在 URL 上加 `?resource_urls=public` | 仅该次请求 |
|
||||
| 整个部署 | 环境变量 `RESOURCE_URL_MODE=public` | 所有未显式传参的请求 |
|
||||
|
||||
`resource_urls` 取值为 `handle`(默认,保持内部引用)或 `public`(返回直链);传其它值返回
|
||||
`400`。单次请求的参数优先于环境变量,因此把部署默认设成 `public` 后,仍可用
|
||||
`?resource_urls=handle` 单独退回。
|
||||
|
||||
支持该参数的接口:
|
||||
|
||||
- `POST /api/v1/knowledge-chat/{session_id}`(SSE)
|
||||
- `POST /api/v1/agent-chat/{session_id}`(SSE)
|
||||
- `GET /api/v1/sessions/continue-stream/{session_id}`(SSE)
|
||||
- `GET /api/v1/messages/{session_id}/load`
|
||||
- `POST /api/v1/knowledge-search`
|
||||
- `POST /api/v1/knowledge-bases/{id}/hybrid-search`(兼容 GET)
|
||||
|
||||
改写覆盖答案正文、检索结果 `content` / `image_info`、`knowledge_references`、Agent 执行步骤与工具结果,以及消息
|
||||
上的图片附件。流式回答里跨两个 chunk 被截断的引用会先缓冲再改写,客户端拿到的始终是完整链接。
|
||||
|
||||
### 注意事项
|
||||
|
||||
- **需要外链能力。** 直链由存储后端预签名,或由 `APP_EXTERNAL_URL` + `/r/<token>` 提供。二者都不
|
||||
可用时(例如 local 存储且未设 `APP_EXTERNAL_URL`),该引用**保持 `resource://` 原样**,客户端
|
||||
仍可回退到 `/files` 代理。详见 `.env.example` 中的 `APP_EXTERNAL_URL` 说明。
|
||||
- **直链是限时匿名可读的**(WeKnora 签发的 grant 2 小时,MinIO 预签名 24 小时)。任何拿到链接的
|
||||
人在过期前都能读取该文件,请勿写入日志或转发给不应看到该文件的一方。
|
||||
- **嵌入式(embed)渠道不支持该参数。** 其访客是匿名的,`/api/v1/embed/...` 下的接口会强制使用
|
||||
`handle`(即使传了 `?resource_urls=public`、或部署默认是 `public`),图片仍走渠道维度的鉴权代理。
|
||||
- **限定知识库的 API Key 不能使用 `public`**,返回 `403`。这类 Key 本身也被拒绝访问 `/files`
|
||||
代理,若能拿到匿名直链等于绕过同一道限制。改用 `handle` 即可正常调用。
|
||||
- **同一文件的直链会在有效期内复用**:重复请求不会反复签发凭证,也不会每次都拿到不同的 URL,客户端
|
||||
和 CDN 的缓存因此可以命中。凭证被吊销或过期后链接立即失效。
|
||||
|
||||
## API 概览
|
||||
|
||||
WeKnora API 按功能分为以下几类:
|
||||
|
||||
| 分类 | 描述 | 文档链接 |
|
||||
|------|------|----------|
|
||||
| 认证管理 | 用户注册、登录、令牌管理;OIDC 流程 | [auth.md](./auth.md) · [OIDC认证调用流程.md](../OIDC认证调用流程.md) |
|
||||
| 空间管理 | 创建和管理空间账户 | [tenant.md](./tenant.md) |
|
||||
| 知识库管理 | 创建、查询和管理知识库 | [knowledge-base.md](./knowledge-base.md) |
|
||||
| 知识管理 | 上传、检索和管理知识内容 | [knowledge.md](./knowledge.md) |
|
||||
| 模型管理 | 配置和管理各种AI模型 | [model.md](./model.md) |
|
||||
| 分块管理 | 管理知识的分块内容 | [chunk.md](./chunk.md) |
|
||||
| 标签管理 | 管理知识库的标签分类 | [tag.md](./tag.md) |
|
||||
| FAQ管理 | 管理FAQ问答对 | [faq.md](./faq.md) |
|
||||
| 智能体管理 | 创建和管理自定义智能体 | [agent.md](./agent.md) |
|
||||
| 会话管理 | 创建和管理对话会话 | [session.md](./session.md) |
|
||||
| 知识搜索 | 在知识库中搜索内容 | [knowledge-search.md](./knowledge-search.md) |
|
||||
| 聊天功能 | 基于知识库和 Agent 进行问答 | [chat.md](./chat.md) |
|
||||
| 消息管理 | 获取和管理对话消息 | [message.md](./message.md) |
|
||||
| 评估功能 | 评估模型性能 | [evaluation.md](./evaluation.md) |
|
||||
| 初始化管理 | 知识库模型配置与 Ollama 管理 | [initialization.md](./initialization.md) |
|
||||
| 系统管理 | 系统信息、解析引擎、存储引擎 | [system.md](./system.md) |
|
||||
| MCP 服务 | MCP 工具服务管理 | [mcp-service.md](./mcp-service.md) |
|
||||
| 组织管理 | 组织、成员、知识库/智能体共享 | [organization.md](./organization.md) |
|
||||
| Skills | 预装与已安装的智能体技能、技能环境变量 | [skill.md](./skill.md) |
|
||||
| 长期记忆 | 当前用户的跨会话记忆(设置、条目、主题、文档亲和度、导出、整理) | [memory.md](./memory.md) |
|
||||
| 网络搜索 | 网络搜索服务商 | [web-search.md](./web-search.md) |
|
||||
| 向量存储 | 向量数据库连接管理 | [vector-store.md](./vector-store.md) |
|
||||
| 存储后端 | 对象/文件存储实例(多实例)管理 | [storage-backend.md](./storage-backend.md) |
|
||||
| IM 渠道 | 企业微信 / 飞书 / Slack 等 IM 平台对接,含渠道 CRUD 与回调 | [../IM集成开发文档.md](../IM集成开发文档.md) |
|
||||
| 数据源导入 | 飞书 / 企微 / Notion / Confluence 等外部数据源接入与同步 | [../数据源导入开发文档.md](../数据源导入开发文档.md) |
|
||||
@@ -0,0 +1,553 @@
|
||||
# 智能体(Agent)管理 API
|
||||
|
||||
[返回目录](./README.md)
|
||||
|
||||
## 概述
|
||||
|
||||
智能体 API 用于管理自定义智能体(Custom Agent)。系统提供了内置智能体,同时支持用户创建自定义智能体来满足不同的业务场景需求。
|
||||
|
||||
> 智能体的共享与跨组织分发(`/agents/:id/shares` 等)属于组织协作能力,文档见 [组织管理 API](./organization.md)。本文件只覆盖智能体自身的 CRUD、复制、占位符、类型预设以及推荐问题接口。
|
||||
|
||||
### 内置智能体
|
||||
|
||||
系统默认提供以下内置智能体:
|
||||
|
||||
| ID | 名称 | 描述 | 模式 |
|
||||
|----|------|------|------|
|
||||
| `builtin-quick-answer` | 快速问答 | 基于知识库的 RAG 问答,快速准确地回答问题 | quick-answer |
|
||||
| `builtin-smart-reasoning` | 智能推理 | ReAct 推理框架,支持多步思考和工具调用 | smart-reasoning |
|
||||
| `builtin-data-analyst` | 数据分析师 | 专业数据分析智能体,支持 CSV/Excel 文件的 SQL 查询与统计分析 | smart-reasoning |
|
||||
|
||||
### 智能体模式
|
||||
|
||||
| 模式 | 说明 |
|
||||
|------|------|
|
||||
| `quick-answer` | RAG 模式,快速问答,直接基于知识库检索结果生成回答 |
|
||||
| `smart-reasoning` | ReAct 模式,支持多步推理和工具调用 |
|
||||
|
||||
## API 列表
|
||||
|
||||
| 方法 | 路径 | 描述 |
|
||||
| ------ | -------------------------- | -------------------------- |
|
||||
| POST | `/agents` | 创建智能体 |
|
||||
| GET | `/agents` | 获取智能体列表 |
|
||||
| GET | `/agents/:id` | 获取智能体详情 |
|
||||
| PUT | `/agents/:id` | 更新智能体 |
|
||||
| DELETE | `/agents/:id` | 删除智能体 |
|
||||
| POST | `/agents/:id/copy` | 复制智能体 |
|
||||
| GET | `/agents/placeholders` | 获取占位符定义 |
|
||||
|
||||
---
|
||||
|
||||
## POST `/agents` - 创建智能体
|
||||
|
||||
创建新的自定义智能体。成功返回 HTTP 201。
|
||||
|
||||
**请求体参数**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ------------- | ------ | ---- | ------------------------------------------------- |
|
||||
| `name` | string | 是 | 智能体名称 |
|
||||
| `description` | string | 否 | 智能体描述 |
|
||||
| `avatar` | string | 否 | 头像(emoji 或图标名称) |
|
||||
| `config` | object | 否 | 智能体配置,详见 [配置参数](#配置参数) |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/agents' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name": "我的智能体",
|
||||
"description": "自定义智能体描述",
|
||||
"avatar": "🤖",
|
||||
"config": {
|
||||
"agent_mode": "smart-reasoning",
|
||||
"system_prompt": "你是一个专业的助手...",
|
||||
"temperature": 0.7,
|
||||
"max_iterations": 10,
|
||||
"kb_selection_mode": "all",
|
||||
"web_search_enabled": true,
|
||||
"multi_turn_enabled": true,
|
||||
"history_turns": 5
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "我的智能体",
|
||||
"description": "自定义智能体描述",
|
||||
"avatar": "🤖",
|
||||
"is_builtin": false,
|
||||
"tenant_id": 1,
|
||||
"created_by": "user-123",
|
||||
"config": {
|
||||
"agent_mode": "smart-reasoning",
|
||||
"system_prompt": "你是一个专业的助手...",
|
||||
"temperature": 0.7,
|
||||
"max_iterations": 10
|
||||
},
|
||||
"created_at": "2025-01-19T10:00:00Z",
|
||||
"updated_at": "2025-01-19T10:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**:
|
||||
|
||||
| 状态码 | 错误码 | 错误 | 说明 |
|
||||
| ------ | ------ | --------------------- | --------------------------------- |
|
||||
| 400 | 1000 | Bad Request | 请求参数错误或智能体名称为空 |
|
||||
| 500 | 1007 | Internal Server Error | 服务器内部错误 |
|
||||
|
||||
---
|
||||
|
||||
## GET `/agents` - 获取智能体列表
|
||||
|
||||
获取当前空间的所有智能体,包括内置智能体和自定义智能体。响应中额外返回 `disabled_own_agent_ids`,指示当前空间在前端对话下拉框中主动隐藏的本空间自有智能体 ID 列表(不影响其他空间)。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/agents' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": "builtin-quick-answer",
|
||||
"name": "快速问答",
|
||||
"description": "基于知识库的 RAG 问答,快速准确地回答问题",
|
||||
"avatar": "💬",
|
||||
"is_builtin": true,
|
||||
"tenant_id": 10000,
|
||||
"created_by": "",
|
||||
"config": {
|
||||
"agent_mode": "quick-answer",
|
||||
"temperature": 0.3,
|
||||
"max_completion_tokens": 2048,
|
||||
"kb_selection_mode": "all",
|
||||
"web_search_enabled": false,
|
||||
"multi_turn_enabled": true,
|
||||
"history_turns": 5
|
||||
},
|
||||
"created_at": "2025-12-29T20:06:01.696308+08:00",
|
||||
"updated_at": "2025-12-29T20:06:01.696308+08:00",
|
||||
"deleted_at": null
|
||||
},
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "我的智能体",
|
||||
"is_builtin": false,
|
||||
"config": {
|
||||
"agent_mode": "smart-reasoning"
|
||||
}
|
||||
}
|
||||
],
|
||||
"disabled_own_agent_ids": []
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**:
|
||||
|
||||
| 状态码 | 错误码 | 错误 | 说明 |
|
||||
| ------ | ------ | --------------------- | ------------------ |
|
||||
| 401 | 1001 | Unauthorized | 缺少空间上下文 |
|
||||
| 500 | 1007 | Internal Server Error | 服务器内部错误 |
|
||||
|
||||
---
|
||||
|
||||
## GET `/agents/:id` - 获取智能体详情
|
||||
|
||||
根据 ID 获取智能体的详细信息。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| ---- | ------ | -------- |
|
||||
| `id` | string | 智能体 ID |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/agents/builtin-quick-answer' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "builtin-quick-answer",
|
||||
"name": "快速问答",
|
||||
"description": "基于知识库的 RAG 问答,快速准确地回答问题",
|
||||
"is_builtin": true,
|
||||
"tenant_id": 1,
|
||||
"config": {
|
||||
"agent_mode": "quick-answer",
|
||||
"system_prompt": "",
|
||||
"context_template": "请根据以下参考资料回答用户问题...",
|
||||
"temperature": 0.7,
|
||||
"max_completion_tokens": 2048,
|
||||
"kb_selection_mode": "all",
|
||||
"web_search_enabled": true,
|
||||
"multi_turn_enabled": true,
|
||||
"history_turns": 5
|
||||
},
|
||||
"created_at": "2025-01-01T00:00:00Z",
|
||||
"updated_at": "2025-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**:
|
||||
|
||||
| 状态码 | 错误码 | 错误 | 说明 |
|
||||
| ------ | ------ | --------------------- | ------------------ |
|
||||
| 400 | 1000 | Bad Request | 智能体 ID 为空 |
|
||||
| 404 | 1003 | Not Found | 智能体不存在 |
|
||||
| 500 | 1007 | Internal Server Error | 服务器内部错误 |
|
||||
|
||||
---
|
||||
|
||||
## PUT `/agents/:id` - 更新智能体
|
||||
|
||||
更新智能体的名称、描述、头像和配置。内置智能体不可修改。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| ---- | ------ | -------- |
|
||||
| `id` | string | 智能体 ID |
|
||||
|
||||
**请求体参数**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ------------- | ------ | ---- | ------------ |
|
||||
| `name` | string | 否 | 智能体名称 |
|
||||
| `description` | string | 否 | 智能体描述 |
|
||||
| `avatar` | string | 否 | 智能体头像 |
|
||||
| `config` | object | 否 | 智能体配置 |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request PUT 'http://localhost:8080/api/v1/agents/550e8400-e29b-41d4-a716-446655440000' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name": "更新后的智能体",
|
||||
"description": "更新后的描述",
|
||||
"config": {
|
||||
"agent_mode": "smart-reasoning",
|
||||
"temperature": 0.8,
|
||||
"max_iterations": 20
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "更新后的智能体",
|
||||
"description": "更新后的描述",
|
||||
"config": {
|
||||
"agent_mode": "smart-reasoning",
|
||||
"temperature": 0.8,
|
||||
"max_iterations": 20
|
||||
},
|
||||
"updated_at": "2025-01-19T11:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**:
|
||||
|
||||
| 状态码 | 错误码 | 错误 | 说明 |
|
||||
| ------ | ------ | --------------------- | --------------------------------- |
|
||||
| 400 | 1000 | Bad Request | 请求参数错误或智能体名称为空 |
|
||||
| 403 | 1002 | Forbidden | 无法修改内置智能体 |
|
||||
| 404 | 1003 | Not Found | 智能体不存在 |
|
||||
| 500 | 1007 | Internal Server Error | 服务器内部错误 |
|
||||
|
||||
---
|
||||
|
||||
## DELETE `/agents/:id` - 删除智能体
|
||||
|
||||
删除指定的自定义智能体。内置智能体不可删除。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| ---- | ------ | -------- |
|
||||
| `id` | string | 智能体 ID |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request DELETE 'http://localhost:8080/api/v1/agents/550e8400-e29b-41d4-a716-446655440000' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Agent deleted successfully"
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**:
|
||||
|
||||
| 状态码 | 错误码 | 错误 | 说明 |
|
||||
| ------ | ------ | --------------------- | --------------------- |
|
||||
| 400 | 1000 | Bad Request | 智能体 ID 为空 |
|
||||
| 403 | 1002 | Forbidden | 无法删除内置智能体 |
|
||||
| 404 | 1003 | Not Found | 智能体不存在 |
|
||||
| 500 | 1007 | Internal Server Error | 服务器内部错误 |
|
||||
|
||||
---
|
||||
|
||||
## POST `/agents/:id/copy` - 复制智能体
|
||||
|
||||
复制指定的智能体,创建一个新的副本,副本始终为自定义智能体。支持复制内置智能体。成功返回 HTTP 201。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| ---- | ------ | -------------- |
|
||||
| `id` | string | 源智能体 ID |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request POST 'http://localhost:8080/api/v1/agents/builtin-smart-reasoning/copy' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "660e8400-e29b-41d4-a716-446655440001",
|
||||
"name": "智能推理 (副本)",
|
||||
"description": "ReAct 推理框架,支持多步思考和工具调用",
|
||||
"is_builtin": false,
|
||||
"config": {
|
||||
"agent_mode": "smart-reasoning",
|
||||
"max_iterations": 50
|
||||
},
|
||||
"created_at": "2025-01-19T12:00:00Z",
|
||||
"updated_at": "2025-01-19T12:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**:
|
||||
|
||||
| 状态码 | 错误码 | 错误 | 说明 |
|
||||
| ------ | ------ | --------------------- | ------------------ |
|
||||
| 400 | 1000 | Bad Request | 智能体 ID 为空 |
|
||||
| 404 | 1003 | Not Found | 智能体不存在 |
|
||||
| 500 | 1007 | Internal Server Error | 服务器内部错误 |
|
||||
|
||||
---
|
||||
|
||||
## GET `/agents/placeholders` - 获取占位符定义
|
||||
|
||||
获取所有可用的提示词占位符定义,按字段类型分组。这些占位符可用于系统提示词和上下文模板中。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/agents/placeholders' \
|
||||
--header 'X-API-Key: your_api_key'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"all": [...],
|
||||
"system_prompt": [...],
|
||||
"agent_system_prompt": [...],
|
||||
"context_template": [...],
|
||||
"rewrite_system_prompt": [...],
|
||||
"rewrite_prompt": [...],
|
||||
"fallback_prompt": [...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 配置参数
|
||||
|
||||
智能体的 `config` 对象支持以下配置项:
|
||||
|
||||
### 基础设置
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `agent_mode` | string | - | 智能体模式:`quick-answer`(RAG)或 `smart-reasoning`(ReAct) |
|
||||
| `system_prompt` | string | - | 系统提示词,支持使用占位符 |
|
||||
| `system_prompt_id` | string | - | 系统提示词模板 ID(引用 `prompt_templates/` YAML 文件中的模板) |
|
||||
| `context_template` | string | - | 上下文模板(仅 quick-answer 模式使用) |
|
||||
| `context_template_id` | string | - | 上下文模板 ID(引用 `prompt_templates/` YAML 文件中的模板) |
|
||||
|
||||
### 模型设置
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `model_id` | string | - | 对话模型 ID |
|
||||
| `rerank_model_id` | string | - | 重排序模型 ID |
|
||||
| `temperature` | float | 0.7 | 温度参数(0-1) |
|
||||
| `max_completion_tokens` | int | 2048 | 最大生成 token 数 |
|
||||
| `thinking` | *bool | nil | 是否启用思考模式(适用于支持扩展思考的模型) |
|
||||
|
||||
### Agent 模式设置
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `max_iterations` | int | 10 | ReAct 最大迭代次数。`-1` 表示不限制,直到模型自然结束或用户停止;正数上限为 100 |
|
||||
| `allowed_tools` | []string | - | 允许使用的工具列表 |
|
||||
| `mcp_selection_mode` | string | - | MCP 服务选择模式:`all`/`selected`/`none` |
|
||||
| `mcp_services` | []string | - | 选中的 MCP 服务 ID 列表 |
|
||||
| `skills_selection_mode` | string | - | Skills 选择模式:`all`/`selected`/`none` |
|
||||
| `selected_skills` | []string | - | 选中的 Skill 名称列表(mode 为 `selected` 时) |
|
||||
|
||||
### 知识库设置
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `kb_selection_mode` | string | - | 知识库选择模式:`all`/`selected`/`none` |
|
||||
| `knowledge_bases` | []string | - | 关联的知识库 ID 列表 |
|
||||
| `retrieve_kb_only_when_mentioned` | bool | false | 仅在用户通过 @ 显式提及时才检索知识库 |
|
||||
| `supported_file_types` | []string | - | 支持的文件类型(如 `["csv", "xlsx"]`) |
|
||||
|
||||
### 图片上传 / 多模态设置
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `image_upload_enabled` | bool | false | 是否允许上传图片 |
|
||||
| `vlm_model_id` | string | - | 图片分析所用的 VLM 模型 ID |
|
||||
| `image_storage_provider` | string | - | 图片存储提供者:`local`/`minio`/`cos`/`tos`/`oss`,为空使用全局默认 |
|
||||
|
||||
### FAQ 策略设置
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `faq_priority_enabled` | bool | true | FAQ 优先策略开关 |
|
||||
| `faq_direct_answer_threshold` | float | 0.9 | FAQ 直接回答阈值 |
|
||||
| `faq_score_boost` | float | 1.2 | FAQ 分数加成系数 |
|
||||
|
||||
### 网络搜索设置
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `web_search_enabled` | bool | true | 是否启用网络搜索 |
|
||||
| `web_search_max_results` | int | 5 | 网络搜索最大结果数 |
|
||||
| `web_search_provider_id` | string | - | 网络搜索提供者 ID,为空使用空间默认提供者 |
|
||||
| `web_fetch_enabled` | bool | false | 是否自动获取重排后的搜索结果页面全文 |
|
||||
| `web_fetch_top_n` | int | 3 | 重排后获取全文的最大页面数 |
|
||||
|
||||
### 多轮对话设置
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `multi_turn_enabled` | bool | true | 是否启用多轮对话 |
|
||||
| `history_turns` | int | 5 | 保留的历史轮次数 |
|
||||
|
||||
### 检索策略设置
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `embedding_top_k` | int | 10 | 向量检索 TopK |
|
||||
| `keyword_threshold` | float | 0.3 | 关键词检索阈值 |
|
||||
| `vector_threshold` | float | 0.5 | 向量检索阈值 |
|
||||
| `rerank_top_k` | int | 5 | 重排序 TopK |
|
||||
| `rerank_threshold` | float | 0.5 | 重排序阈值 |
|
||||
|
||||
### 推荐问题设置
|
||||
|
||||
`question_suggestions` 是智能体拥有的统一策略。网页嵌入等渠道只能关闭展示,不能覆盖内容或生成规则。
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `question_suggestions.starters.enabled` | bool | true | 是否在首次提问前展示开场问题 |
|
||||
| `question_suggestions.starters.mode` | string | `hybrid` | `curated`、`knowledge` 或 `hybrid` |
|
||||
| `question_suggestions.starters.items` | []string | `[]` | 运营配置的开场问题 |
|
||||
| `question_suggestions.starters.count` | int | 6 | 展示数量,范围 1-8 |
|
||||
| `question_suggestions.follow_ups.enabled` | bool | false | 是否在每次完整回答后异步生成追问 |
|
||||
| `question_suggestions.follow_ups.mode` | string | `hybrid` | `generated`、`knowledge` 或 `hybrid` |
|
||||
| `question_suggestions.follow_ups.count` | int | 3 | 生成数量,范围 1-5 |
|
||||
| `question_suggestions.follow_ups.model_id` | string | - | 独立生成模型;为空使用本轮对话模型 |
|
||||
| `question_suggestions.follow_ups.categories` | []string | `clarify,deepen,action` | 允许的问题类型 |
|
||||
| `question_suggestions.follow_ups.max_context_turns` | int | 2 | 生成时使用的最近对话轮数,范围 1-5 |
|
||||
| `question_suggestions.follow_ups.additional_instruction` | string | - | 智能体作者的附加生成要求 |
|
||||
| `question_suggestions.follow_ups.suppress_on_fallback` | bool | true | 兜底回答后不展示 |
|
||||
| `question_suggestions.follow_ups.suppress_when_answer_asks_question` | bool | true | 回答本身以问题结尾时不展示 |
|
||||
| `question_suggestions.follow_ups.knowledge_fallback` | bool | true | 模型失败时使用知识库候选补位 |
|
||||
| `question_suggestions.follow_ups.allow_regenerate` | bool | false | 是否允许用户换一批 |
|
||||
|
||||
旧 `suggested_prompts` 会在数据库迁移时一次性写入 `starters.items`,API 不再接受该字段。
|
||||
|
||||
### 高级设置
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `enable_query_expansion` | bool | true | 是否启用查询扩展 |
|
||||
| `enable_rewrite` | bool | true | 是否启用多轮对话查询改写 |
|
||||
| `rewrite_prompt_system` | string | - | 改写系统提示词 |
|
||||
| `rewrite_prompt_user` | string | - | 改写用户提示词模板 |
|
||||
| `fallback_strategy` | string | `model` | 回退策略:`fixed`(固定回复)或 `model`(模型生成);未设置时在服务端默认为 `model` |
|
||||
| `fallback_response` | string | - | 固定回退回复(`fallback_strategy` 为 `fixed` 时使用) |
|
||||
| `fallback_prompt` | string | - | 回退提示词(`fallback_strategy` 为 `model` 时使用) |
|
||||
|
||||
---
|
||||
|
||||
## 使用 Agent 进行问答
|
||||
|
||||
创建或获取智能体后,可以通过 `/agent-chat/:session_id` 接口使用智能体进行问答。详情请参考 [聊天功能 API](./chat.md)。
|
||||
|
||||
在问答请求中使用 `agent_id` 参数指定要使用的智能体:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/agent-chat/session-123' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"query": "帮我分析一下这份数据",
|
||||
"agent_enabled": true,
|
||||
"agent_id": "builtin-data-analyst"
|
||||
}'
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- 智能体的组织共享、跨空间分发与禁用(`/agents/:id/shares`、`/shared-agents` 等):见 [组织管理 API](./organization.md)
|
||||
- 智能体绑定 IM 渠道(`/agents/:id/im-channels`):见组织/IM 渠道相关文档
|
||||
- 网络搜索提供者配置(被 `web_search_provider_id` 引用):见 [Web Search API](./web-search.md)
|
||||
@@ -0,0 +1,449 @@
|
||||
# 认证管理 API
|
||||
|
||||
[返回目录](./README.md)
|
||||
|
||||
OIDC 完整调用流程见 [../OIDC认证调用流程.md](../OIDC认证调用流程.md)。本文档作为端点参考。
|
||||
|
||||
## 说明
|
||||
|
||||
WeKnora 的 `/auth/*` 端点本身**不需要 X-API-Key**,但部分端点需要在 `Authorization: Bearer <token>` 头中携带由 `/auth/login` 或 `/auth/oidc/callback` 返回的 JWT:
|
||||
|
||||
| 端点 | 鉴权方式 |
|
||||
| --- | --- |
|
||||
| `/auth/register` `/auth/login` `/auth/config` | 无 |
|
||||
| `/auth/oidc/config` `/auth/oidc/url` `/auth/oidc/callback` | 无 |
|
||||
| `/auth/refresh` | refresh_token(请求体携带) |
|
||||
| `/auth/validate` `/auth/me` `/auth/logout` `/auth/change-password` `/auth/switch-tenant` `/auth/me/preferences` | Bearer JWT |
|
||||
|
||||
注册接口可通过环境变量 `DISABLE_REGISTRATION=true` 关闭。密码策略默认 8–32 位且同时包含字母与数字;部署可通过环境变量 `WEKNORA_AUTH_COMPLEX_PASSWORD_ENABLED` 或系统设置 `auth.complex_password_enabled` 要求额外包含大小写字母与特殊字符。当前策略见 `GET /auth/config`。
|
||||
|
||||
## 端点一览
|
||||
|
||||
| 方法 | 路径 | 描述 |
|
||||
| ---- | -------------------------- | ------------------------------------------ |
|
||||
| GET | `/auth/config` | 公开认证配置(注册模式、密码复杂度) |
|
||||
| POST | `/auth/register` | 用户注册 |
|
||||
| POST | `/auth/login` | 用户登录 |
|
||||
| GET | `/auth/oidc/config` | 获取 OIDC 配置元数据 |
|
||||
| GET | `/auth/oidc/url` | 获取 OIDC 授权链接 |
|
||||
| GET | `/auth/oidc/callback` | OIDC 授权回调(由 IdP 重定向触发) |
|
||||
| POST | `/auth/refresh` | 用 refresh_token 换新的 access_token |
|
||||
| GET | `/auth/validate` | 验证 JWT 有效性 |
|
||||
| POST | `/auth/logout` | 退出登录 |
|
||||
| GET | `/auth/me` | 获取当前用户信息 |
|
||||
| PUT | `/auth/me/preferences` | 更新最近活跃空间等个人偏好 |
|
||||
| POST | `/auth/switch-tenant` | 切换激活空间并换发 token |
|
||||
| POST | `/auth/change-password` | 修改密码 |
|
||||
|
||||
---
|
||||
|
||||
## GET `/auth/config` - 公开认证配置
|
||||
|
||||
无需登录。前端用它决定是否展示注册入口,以及注册/改密表单应使用哪套密码规则。
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"registration_mode": "self_serve",
|
||||
"complex_password_enabled": false
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 说明 |
|
||||
| ---- | ---- |
|
||||
| `registration_mode` | `self_serve` 允许公开注册;`invite_only` 仅邀请 |
|
||||
| `complex_password_enabled` | `true` 时新密码须含大小写字母、数字和特殊字符 `!@#$%^&*()_+-=[]{}|;:,.<>?` |
|
||||
|
||||
---
|
||||
|
||||
## POST `/auth/register` - 用户注册
|
||||
|
||||
**参数说明(请求体)**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 校验 | 说明 |
|
||||
| -------- | ------ | ---- | -------------------------- | --------- |
|
||||
| username | string | 是 | 长度 2-50 | 用户名 |
|
||||
| email | string | 是 | 邮箱格式 | 邮箱 |
|
||||
| password | string | 是 | 8–32 位,须含字母与数字;若 `complex_password_enabled` 则还须含大小写与特殊字符 | 密码 |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/auth/register' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"username": "alice",
|
||||
"email": "alice@example.com",
|
||||
"password": "secret123"
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**(201 Created):
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Registration successful",
|
||||
"user": {
|
||||
"id": "usr-...",
|
||||
"username": "alice",
|
||||
"email": "alice@example.com",
|
||||
"tenant_id": 1,
|
||||
"is_active": true,
|
||||
"created_at": "2026-05-11T10:00:00+08:00",
|
||||
"updated_at": "2026-05-11T10:00:00+08:00"
|
||||
},
|
||||
"tenant": {
|
||||
"id": 1,
|
||||
"name": "alice's workspace",
|
||||
"api_key": "sk-..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**错误**: 注册被禁用 → 403;参数校验失败 → 400。
|
||||
|
||||
---
|
||||
|
||||
## POST `/auth/login` - 用户登录
|
||||
|
||||
**参数说明(请求体)**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| -------- | ------ | ---- | ------------- |
|
||||
| email | string | 是 | 注册邮箱 |
|
||||
| password | string | 是 | 密码 |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/auth/login' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"email": "alice@example.com",
|
||||
"password": "secret123"
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Login successful",
|
||||
"user": { "id": "usr-...", "username": "alice", "email": "alice@example.com" },
|
||||
"tenant": { "id": 1, "name": "alice's workspace", "api_key": "sk-..." },
|
||||
"token": "eyJhbGciOi...",
|
||||
"refresh_token": "eyJhbGciOi..."
|
||||
}
|
||||
```
|
||||
|
||||
**错误**: 邮箱或密码错误 → 401;账号被禁用 → 403。
|
||||
|
||||
---
|
||||
|
||||
## GET `/auth/oidc/config` - 获取 OIDC 配置元数据
|
||||
|
||||
返回 OIDC 是否启用以及 Provider 显示名,前端登录页据此决定是否展示 OIDC 登录按钮。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/auth/oidc/config'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"enabled": true,
|
||||
"provider_display_name": "WeKnora SSO"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GET `/auth/oidc/url` - 获取 OIDC 授权链接
|
||||
|
||||
返回前端应跳转的 OIDC IdP 授权页 URL 与状态码。
|
||||
|
||||
**查询参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ---------- | ------ | ---- | ------------------------------------------------------- |
|
||||
| redirect | string | 否 | 登录成功后前端期望落地的路径(如 `/dashboard`),透传到 state |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/auth/oidc/url?redirect=%2Fdashboard'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"provider_display_name": "WeKnora SSO",
|
||||
"authorization_url": "https://idp.example.com/oauth/authorize?client_id=...&state=...",
|
||||
"state": "abcdef..."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GET `/auth/oidc/callback` - OIDC 授权回调
|
||||
|
||||
由 IdP 在用户授权后重定向到此端点。一般不需要客户端代码直接调用——它的作用是把登录结果通过浏览器 hash 传回前端首页。
|
||||
|
||||
**查询参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ----------------------------- |
|
||||
| code | string | 是 | IdP 颁发的 authorization code |
|
||||
| state | string | 是 | 与 `/auth/oidc/url` 返回值一致 |
|
||||
| error | string | 否 | IdP 返回的错误标识 |
|
||||
| error_description | string | 否 | IdP 返回的错误详情 |
|
||||
|
||||
**响应**:始终返回 `302 Found`,跳转到 `/`,并把结果编码进 URL hash:
|
||||
|
||||
- 成功:`/#oidc_result=<base64url(JSON payload)>`,其中 payload 包含 `success` / `user` / `tenant` / `token` / `refresh_token` / `is_new_user`,与登录响应一致。
|
||||
- 失败:`/#oidc_error=<reason>[&oidc_error_description=<message>]`,常见 reason 包括 `invalid_state`、`missing_code`、`login_failed`、`payload_encode_failed`。
|
||||
|
||||
---
|
||||
|
||||
## GET `/auth/oidc/start` - 发起 OIDC 登录(直接 302)
|
||||
|
||||
与 `/auth/oidc/url` 不同,此端点**直接 302 重定向**到 OIDC Provider 的授权页,不返回 JSON,因此无需前端 JS 介入。适用于外部平台(如企业门户 / Nexus)直接给出一个链接即可触发 OIDC 授权码流程,借助 IdP 的 SSO session 实现免再次输密码。
|
||||
|
||||
回调地址由后端根据请求自身的 origin(`<scheme>://<host>/api/v1/auth/oidc/callback`)自动构造,无需调用方提供。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/auth/oidc/start'
|
||||
```
|
||||
|
||||
**响应**:`302 Found`,`Location` 指向 IdP 授权页(含 `client_id` / `state` / `redirect_uri` / `scope`)。
|
||||
|
||||
> 登录成功后的回调行为与 `/auth/oidc/callback` 一致:302 回前端首页并把登录结果编码进 URL hash。当前登录后固定落到默认首页 `/platform/knowledge-bases`(直达指定业务页的 `next` 参数为未来扩展)。
|
||||
|
||||
---
|
||||
|
||||
## POST `/auth/refresh` - 刷新令牌
|
||||
|
||||
**参数说明(请求体)**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ------------- | ------ | ---- | ----------------- |
|
||||
| refreshToken | string | 是 | 登录时颁发的 refresh_token |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/auth/refresh' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"refreshToken": "eyJhbGciOi..."
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Token refreshed successfully",
|
||||
"access_token": "eyJhbGciOi...",
|
||||
"refresh_token": "eyJhbGciOi..."
|
||||
}
|
||||
```
|
||||
|
||||
**错误**: refresh_token 无效或过期 → 401。
|
||||
|
||||
---
|
||||
|
||||
## GET `/auth/validate` - 验证 JWT
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/auth/validate' \
|
||||
--header 'Authorization: Bearer eyJhbGciOi...'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"valid": true,
|
||||
"user_id": "usr-...",
|
||||
"tenant_id": 1
|
||||
}
|
||||
```
|
||||
|
||||
无效 token 返回 401。
|
||||
|
||||
---
|
||||
|
||||
## POST `/auth/logout` - 退出登录
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request POST 'http://localhost:8080/api/v1/auth/logout' \
|
||||
--header 'Authorization: Bearer eyJhbGciOi...'
|
||||
```
|
||||
|
||||
**响应**: `{ "success": true, "message": "Logged out successfully" }`
|
||||
|
||||
---
|
||||
|
||||
## GET `/auth/me` - 获取当前用户信息
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/auth/me' \
|
||||
--header 'Authorization: Bearer eyJhbGciOi...'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"user": {
|
||||
"id": "usr-...",
|
||||
"username": "alice",
|
||||
"email": "alice@example.com",
|
||||
"avatar": "",
|
||||
"tenant_id": 1,
|
||||
"is_active": true,
|
||||
"can_access_all_tenants": false,
|
||||
"created_at": "2026-05-11T10:00:00+08:00",
|
||||
"updated_at": "2026-05-11T10:00:00+08:00"
|
||||
},
|
||||
"tenant": {
|
||||
"id": 1,
|
||||
"name": "My Workspace"
|
||||
},
|
||||
"memberships": [
|
||||
{
|
||||
"tenant_id": 1,
|
||||
"tenant_name": "My Workspace",
|
||||
"role": "owner"
|
||||
}
|
||||
],
|
||||
"tenant_required": false,
|
||||
"capabilities": {
|
||||
"can_create_tenant": false,
|
||||
"auto_accept_invitation": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`capabilities` 供 SPA 读取部署级开关,无需调用超管设置 API:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| ---- | ---- |
|
||||
| `can_create_tenant` | 当前用户是否可自助创建空间 |
|
||||
| `auto_accept_invitation` | 全局 `tenant.auto_accept_invitation`:邮箱邀请已注册用户时是否直接加入(无需收件箱确认) |
|
||||
|
||||
---
|
||||
|
||||
## POST `/auth/switch-tenant` - 切换激活空间
|
||||
|
||||
为当前用户在目标空间重新签发 access / refresh token 对。调用者须在目标空间有 **active** 成员关系(`CanAccessAllTenants` 超级用户切到非 home 空间除外)。
|
||||
|
||||
成功换签会把目标空间写入账号级「最近活跃租户」偏好(`users.preferences.last_active_tenant_id`)。refresh JWT **不含** `tenant_id`,因此 **下次登录与 refresh 都按该偏好落点**;一次换签会改变该用户所有设备的落点。偏好写入失败则整次换签失败,**不会**发出新 token。
|
||||
|
||||
切回 home 时服务端写入 home ID(与 SPA 发送 `0` 清偏好在当前落点语义上等价)。Web UI 切空间走 `X-Tenant-ID` + `PUT /auth/me/preferences`,不调用本接口。
|
||||
|
||||
**参数说明(请求体)**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ------------- | ------ | ---- | ---------------------------- |
|
||||
| tenant_id | uint64 | 是 | 目标空间 ID |
|
||||
| refresh_token | string | 否 | 当前 refresh token,成功后撤销 |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request POST 'http://localhost:8080/api/v1/auth/switch-tenant' \
|
||||
--header 'Authorization: Bearer eyJhbGciOi...' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"tenant_id": 2
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**: 与登录相同的 `LoginResponse`(`user` / `active_tenant` / `memberships` / `token` / `refresh_token`)。`user.preferences.last_active_tenant_id` 与目标空间一致。
|
||||
|
||||
**错误**: 无成员关系或偏好写入失败 → 403;参数校验失败 → 400。
|
||||
|
||||
---
|
||||
|
||||
## PUT `/auth/me/preferences` - 更新个人偏好
|
||||
|
||||
按 PATCH 语义合并 `users.preferences`(仅覆盖请求体里出现的字段)。SPA 在 UI 切空间后用此接口记住落点;`POST /auth/switch-tenant` 会在服务端写同一字段,API 客户端不必再补发本请求。
|
||||
|
||||
**参数说明(请求体)**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ---------------------- | ------- | ---- | ---- |
|
||||
| last_active_tenant_id | *uint64 | 否 | 正整数 = 设置/替换;`0` = 清除(下次登录回 home);省略 = 不改 |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request PUT 'http://localhost:8080/api/v1/auth/me/preferences' \
|
||||
--header 'Authorization: Bearer eyJhbGciOi...' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"last_active_tenant_id": 2
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**: `{ "success": true, "data": { "last_active_tenant_id": 2 } }`
|
||||
|
||||
---
|
||||
|
||||
## POST `/auth/change-password` - 修改密码
|
||||
|
||||
修改当前用户的登录密码。新密码须满足 **8–32 位**且**同时包含字母与数字**;当 `GET /auth/config` 的 `complex_password_enabled` 为 true 时,还须包含大小写字母与特殊字符。不能与当前密码相同。成功后**所有会话被撤销**,需使用新密码重新登录。
|
||||
|
||||
**参数说明(请求体)**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 校验 | 说明 |
|
||||
| ------------- | ------ | ---- | ------- | --------- |
|
||||
| old_password | string | 是 | | 当前密码 |
|
||||
| new_password | string | 是 | 8–32 位,须含字母与数字(复杂模式另需大小写与特殊字符),且不同于旧密码 | 新密码 |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/auth/change-password' \
|
||||
--header 'Authorization: Bearer eyJhbGciOi...' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"old_password": "secret123",
|
||||
"new_password": "newsecret456"
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**: `{ "success": true, "message": "Password changed successfully" }`
|
||||
|
||||
**错误**(400):
|
||||
|
||||
| `error.details` | 含义 |
|
||||
| --------------------- | ---------------------------- |
|
||||
| `invalid_old_password` | 当前密码不正确 |
|
||||
| `password_policy` | 新密码不满足长度/复杂度要求 |
|
||||
| `same_password` | 新密码与当前密码相同 |
|
||||
@@ -0,0 +1,190 @@
|
||||
# 聊天功能 API
|
||||
|
||||
[返回目录](./README.md)
|
||||
|
||||
| 方法 | 路径 | 描述 |
|
||||
| ---- | ----------------------------- | ------------------------ |
|
||||
| POST | `/knowledge-chat/:session_id` | 基于知识库的问答 |
|
||||
| POST | `/agent-chat/:session_id` | 基于 Agent 的智能问答 |
|
||||
| POST | `/knowledge-search` | 基于知识库的搜索知识 |
|
||||
| GET | `/sessions/:session_id/messages/:message_id/suggestions` | 获取已生成的回答后推荐 |
|
||||
| POST | `/sessions/:session_id/messages/:message_id/suggestions` | 确保生成或换一批推荐 |
|
||||
| POST | `/sessions/:session_id/suggestion-events` | 上报曝光、点击、关闭事件 |
|
||||
|
||||
## POST `/knowledge-chat/:session_id` - 基于知识库的问答
|
||||
|
||||
基于知识库的 RAG 问答,支持 SSE 流式响应。
|
||||
|
||||
**查询参数**:
|
||||
|
||||
| 参数 | 取值 | 说明 |
|
||||
|------|------|------|
|
||||
| `resource_urls` | `handle`(默认)/ `public` | `public` 让答案与引用里的图片直接返回可加载的 http(s) 链接,省去逐个调用 `/files` 代理。详见[文件与图片引用](./README.md#文件与图片引用resource-与直链) |
|
||||
|
||||
同样适用于下面的 `/agent-chat/:session_id`、`/knowledge-search`、`/knowledge-bases/:id/hybrid-search` 与 `/sessions/continue-stream/:session_id`。
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `query` | string | 是 | 查询文本 |
|
||||
| `knowledge_base_ids` | string[] | 否 | 知识库 ID 列表 |
|
||||
| `knowledge_ids` | string[] | 否 | 知识文件 ID 列表,指定具体文件进行检索 |
|
||||
| `agent_id` | string | 否 | 自定义 Agent ID,指定使用的智能体 |
|
||||
| `summary_model_id` | string | 否 | 覆盖默认的摘要模型 ID |
|
||||
| `mentioned_items` | object[] | 否 | @提及的知识库和文件列表 |
|
||||
| `disable_title` | bool | 否 | 是否禁用自动标题生成(默认 false) |
|
||||
| `images` | object[] | 否 | 附带的图片(base64 格式),需要 Agent 启用图片上传 |
|
||||
| `channel` | string | 否 | 来源渠道标识:`web`、`api`、`im`、`browser_extension` |
|
||||
| `suggestion_attribution` | object | 否 | 用户从推荐问题发起本轮时传入 `{suggestion_set_id, question_id}`;服务端会校验归属 |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/knowledge-chat/ceb9babb-1e30-41d7-817d-fd584954304b' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"query": "彗尾的形状",
|
||||
"knowledge_base_ids": ["kb-00000001"],
|
||||
"agent_id": "builtin-quick-answer"
|
||||
}'
|
||||
```
|
||||
|
||||
**响应格式**:
|
||||
服务器端事件流(Server-Sent Events,Content-Type: text/event-stream)
|
||||
|
||||
**响应**:
|
||||
|
||||
```
|
||||
event: message
|
||||
data: {"id":"3475c004-0ada-4306-9d30-d7f5efce50d2","response_type":"references","content":"","done":false,"knowledge_references":[{"id":"c8347bef-...","content":"彗星xxx。","knowledge_id":"a6790b93-...","chunk_index":0,"knowledge_title":"彗星.txt","score":4.04,"match_type":3,"chunk_type":"text","knowledge_filename":"彗星.txt"}]}
|
||||
|
||||
event: message
|
||||
data: {"id":"3475c004-0ada-4306-9d30-d7f5efce50d2","response_type":"answer","content":"彗尾的形状主要表现为...","done":false,"knowledge_references":null}
|
||||
|
||||
event: message
|
||||
data: {"id":"3475c004-0ada-4306-9d30-d7f5efce50d2","response_type":"answer","content":"","done":true,"knowledge_references":null}
|
||||
```
|
||||
|
||||
## POST `/agent-chat/:session_id` - 基于 Agent 的智能问答
|
||||
|
||||
Agent 模式支持更智能的问答,包括工具调用、网络搜索、多知识库检索等能力。
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `query` | string | 是 | 查询文本 |
|
||||
| `knowledge_base_ids` | string[] | 否 | 知识库 ID 列表,可动态指定本次查询使用的知识库 |
|
||||
| `knowledge_ids` | string[] | 否 | 知识文件 ID 列表,可动态指定本次查询使用的具体文件 |
|
||||
| `agent_enabled` | bool | 否 | 是否启用 Agent 模式(默认 false,优先使用 Agent 配置) |
|
||||
| `agent_id` | string | 否 | 自定义 Agent ID,指定使用的智能体(支持共享 Agent) |
|
||||
| `web_search_enabled` | bool | 否 | 是否启用网络搜索(默认 false) |
|
||||
| `summary_model_id` | string | 否 | 覆盖默认的摘要模型 ID |
|
||||
| `mentioned_items` | object[] | 否 | @提及的知识库和文件列表 |
|
||||
| `disable_title` | bool | 否 | 是否禁用自动标题生成(默认 false) |
|
||||
| `images` | object[] | 否 | 附带的图片(base64 格式),需要 Agent 启用图片上传 |
|
||||
| `channel` | string | 否 | 来源渠道标识:`web`、`api`、`im`、`browser_extension` |
|
||||
| `suggestion_attribution` | object | 否 | 用户从推荐问题发起本轮时传入 `{suggestion_set_id, question_id}`;服务端会校验归属 |
|
||||
|
||||
## 回答后推荐问题
|
||||
|
||||
回答主消息完成后,服务端会异步生成推荐问题,不阻塞 SSE 的 `complete`/`done` 事件。生成结果按“空间、助手消息、位置、配置快照、语言”持久化并去重。
|
||||
|
||||
```http
|
||||
POST /api/v1/sessions/{session_id}/messages/{message_id}/suggestions
|
||||
Content-Type: application/json
|
||||
|
||||
{"regenerate": false}
|
||||
```
|
||||
|
||||
状态包括 `generating`、`ready`、`suppressed`、`failed`。`ready` 时的每个问题都有稳定 `id`,点击后应先上报事件,并在下一次聊天请求中携带 `suggestion_attribution`。
|
||||
|
||||
```http
|
||||
POST /api/v1/sessions/{session_id}/suggestion-events
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"suggestion_set_id": "...",
|
||||
"question_id": "...",
|
||||
"event_type": "click"
|
||||
}
|
||||
```
|
||||
|
||||
网页嵌入提供同构接口:`/api/v1/embed/{channel_id}/sessions/{session_id}/...`,继续使用嵌入令牌和 `X-Embed-Session`。
|
||||
|
||||
**mentioned_items 结构**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `id` | string | 知识库或文件 ID |
|
||||
| `name` | string | 显示名称 |
|
||||
| `type` | string | 类型:`kb`(知识库)或 `file`(文件) |
|
||||
| `kb_type` | string | 知识库类型:`document` 或 `faq`(仅 `type=kb` 时) |
|
||||
|
||||
**images 结构**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `data` | string | base64 编码的图片数据(`data:image/png;base64,...`) |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/agent-chat/ceb9babb-1e30-41d7-817d-fd584954304b' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"query": "帮我查询今天的天气",
|
||||
"agent_enabled": true,
|
||||
"web_search_enabled": true,
|
||||
"knowledge_base_ids": ["kb-00000001"],
|
||||
"agent_id": "builtin-smart-reasoning",
|
||||
"mentioned_items": [
|
||||
{
|
||||
"id": "kb-00000001",
|
||||
"name": "天气知识库",
|
||||
"type": "kb",
|
||||
"kb_type": "document"
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
**响应格式**:
|
||||
服务器端事件流(Server-Sent Events,Content-Type: text/event-stream)
|
||||
|
||||
**响应类型说明**:
|
||||
|
||||
| response_type | 描述 |
|
||||
|---------------|------|
|
||||
| `agent_query` | Agent 开始处理查询 |
|
||||
| `thinking` | Agent 思考过程 |
|
||||
| `tool_call` | 工具调用信息 |
|
||||
| `tool_result` | 工具调用结果 |
|
||||
| `references` | 知识库检索引用 |
|
||||
| `answer` | 最终回答内容 |
|
||||
| `artifacts_pending` | Skill/沙箱产物正在上传;`data.count` 为待保存文件数。回答可能已经 `done`,文件按钮会在此期间显示加载态,直至 `complete` 带上 `artifacts` |
|
||||
| `reflection` | Agent 反思内容 |
|
||||
| `session_title` | 自动生成的会话标题 |
|
||||
| `error` | 错误信息 |
|
||||
|
||||
**响应示例**:
|
||||
|
||||
```
|
||||
event: message
|
||||
data: {"id":"req-001","response_type":"thinking","content":"用户想查询天气,我需要使用网络搜索工具...","done":false}
|
||||
|
||||
event: message
|
||||
data: {"id":"req-001","response_type":"tool_call","content":"","done":false,"data":{"tool_name":"web_search","arguments":{"query":"今天天气"}}}
|
||||
|
||||
event: message
|
||||
data: {"id":"req-001","response_type":"tool_result","content":"搜索结果:今天晴,气温25°C...","done":false}
|
||||
|
||||
event: message
|
||||
data: {"id":"req-001","response_type":"answer","content":"根据查询结果,今天天气晴朗,气温约25°C。","done":false}
|
||||
|
||||
event: message
|
||||
data: {"id":"req-001","response_type":"answer","content":"","done":true}
|
||||
```
|
||||
@@ -0,0 +1,220 @@
|
||||
# 分块管理 API
|
||||
|
||||
[返回目录](./README.md)
|
||||
|
||||
| 方法 | 路径 | 描述 |
|
||||
| ------ | --------------------------------- | -------------------------- |
|
||||
| GET | `/chunks/:knowledge_id` | 获取知识的分块列表 |
|
||||
| PUT | `/chunks/:knowledge_id/:id` | 更新分块 |
|
||||
| DELETE | `/chunks/:knowledge_id/:id` | 删除单个分块 |
|
||||
| DELETE | `/chunks/:knowledge_id` | 删除知识下的所有分块 |
|
||||
| GET | `/chunks/by-id/:id` | 根据分块 ID 直接获取分块 |
|
||||
| DELETE | `/chunks/by-id/:id/questions` | 删除分块下的某个生成问题 |
|
||||
|
||||
## GET `/chunks/:knowledge_id` - 获取知识的分块列表
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ------------- | ------ | ----------- |
|
||||
| knowledge_id | string | 知识 ID |
|
||||
|
||||
**查询参数**:
|
||||
|
||||
| 字段 | 类型 | 默认 | 说明 |
|
||||
| ---------- | ---- | ---- | ---------- |
|
||||
| page | int | 1 | 页码 |
|
||||
| page_size | int | 20 | 每页条数 |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/chunks/4c4e7c1a-09cf-485b-a7b5-24b8cdc5acf5?page=1&page_size=1' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "df10b37d-cd05-4b14-ba8a-e1bd0eb3bbd7",
|
||||
"tenant_id": 1,
|
||||
"knowledge_id": "4c4e7c1a-09cf-485b-a7b5-24b8cdc5acf5",
|
||||
"knowledge_base_id": "kb-00000001",
|
||||
"tag_id": "",
|
||||
"content": "彗星xxxx",
|
||||
"chunk_index": 0,
|
||||
"is_enabled": true,
|
||||
"status": 2,
|
||||
"start_at": 0,
|
||||
"end_at": 964,
|
||||
"pre_chunk_id": "",
|
||||
"next_chunk_id": "",
|
||||
"chunk_type": "text",
|
||||
"parent_chunk_id": "",
|
||||
"relation_chunks": null,
|
||||
"indirect_relation_chunks": null,
|
||||
"metadata": null,
|
||||
"content_hash": "",
|
||||
"image_info": "",
|
||||
"created_at": "2025-08-12T11:52:36.168632+08:00",
|
||||
"updated_at": "2025-08-12T11:52:53.376871+08:00",
|
||||
"deleted_at": null
|
||||
}
|
||||
],
|
||||
"page": 1,
|
||||
"page_size": 1,
|
||||
"success": true,
|
||||
"total": 5
|
||||
}
|
||||
```
|
||||
|
||||
## PUT `/chunks/:knowledge_id/:id` - 更新分块
|
||||
|
||||
更新指定分块的内容和属性。所有字段均可选,未传则保留原值。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ------------- | ------ | ----------- |
|
||||
| knowledge_id | string | 知识 ID |
|
||||
| id | string | 分块 ID |
|
||||
|
||||
**参数说明(请求体)**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ------------ | ------- | ---- | --------------------- |
|
||||
| content | string | 否 | 分块内容 |
|
||||
| chunk_index | int | 否 | 分块在知识中的序号 |
|
||||
| is_enabled | boolean | 否 | 是否启用 |
|
||||
| start_at | int | 否 | 起始位置(字符偏移) |
|
||||
| end_at | int | 否 | 结束位置(字符偏移) |
|
||||
| image_info | string | 否 | 图像分块的元信息(JSON 字符串) |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request PUT 'http://localhost:8080/api/v1/chunks/4c4e7c1a-09cf-485b-a7b5-24b8cdc5acf5/df10b37d-cd05-4b14-ba8a-e1bd0eb3bbd7' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"content": "更新后的分块内容",
|
||||
"is_enabled": true
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "df10b37d-cd05-4b14-ba8a-e1bd0eb3bbd7",
|
||||
"content": "更新后的分块内容",
|
||||
"is_enabled": true,
|
||||
"...": "其他字段同 GET 响应"
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## DELETE `/chunks/:knowledge_id/:id` - 删除单个分块
|
||||
|
||||
**路径参数**: 同 PUT。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request DELETE 'http://localhost:8080/api/v1/chunks/4c4e7c1a-09cf-485b-a7b5-24b8cdc5acf5/df10b37d-cd05-4b14-ba8a-e1bd0eb3bbd7' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Chunk deleted",
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## DELETE `/chunks/:knowledge_id` - 删除知识下的所有分块
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ------------- | ------ | ----------- |
|
||||
| knowledge_id | string | 知识 ID |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request DELETE 'http://localhost:8080/api/v1/chunks/4c4e7c1a-09cf-485b-a7b5-24b8cdc5acf5' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "All chunks under knowledge deleted",
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## GET `/chunks/by-id/:id` - 根据 ID 直接获取分块
|
||||
|
||||
无需提供 `knowledge_id` 即可获取分块。常用于跨知识库的引用展示。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ---- | ------ | ------- |
|
||||
| id | string | 分块 ID |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/chunks/by-id/df10b37d-cd05-4b14-ba8a-e1bd0eb3bbd7' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应**: 同 `GET /chunks/:knowledge_id` 列表中的单条 data。
|
||||
|
||||
## DELETE `/chunks/by-id/:id/questions` - 删除分块下的某个生成问题
|
||||
|
||||
删除指定分块关联的某条生成问题。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ---- | ------ | ------- |
|
||||
| id | string | 分块 ID |
|
||||
|
||||
**参数说明(请求体)**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ----------- | ------ | ---- | ----------- |
|
||||
| question_id | string | 是 | 问题 ID |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request DELETE 'http://localhost:8080/api/v1/chunks/by-id/df10b37d-cd05-4b14-ba8a-e1bd0eb3bbd7/questions' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"question_id": "q-00000001"
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Question deleted successfully",
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,161 @@
|
||||
# 评估功能 API
|
||||
|
||||
[返回目录](./README.md)
|
||||
|
||||
| 方法 | 路径 | 描述 |
|
||||
| ---- | -------------- | --------------------- |
|
||||
| GET | `/evaluation/` | 获取评估任务结果 |
|
||||
| POST | `/evaluation/` | 创建评估任务 |
|
||||
|
||||
> 注:服务端路由带尾斜杠(Gin 会自动从 `/evaluation` 重定向到 `/evaluation/`),下方示例为方便阅读用了 `/evaluation`。
|
||||
|
||||
## GET `/evaluation` - 获取评估任务结果
|
||||
|
||||
**参数说明(查询参数)**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| -------- | ------ | ---- | --------------------------------------------------- |
|
||||
| task_id | string | 是 | 从 `POST /evaluation` 返回的任务 ID |
|
||||
|
||||
**请求**:
|
||||
|
||||
```bash
|
||||
curl --location 'http://localhost:8080/api/v1/evaluation?task_id=c34563ad-b09f-4858-b72e-e92beb80becb' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"task": {
|
||||
"id": "c34563ad-b09f-4858-b72e-e92beb80becb",
|
||||
"tenant_id": 1,
|
||||
"dataset_id": "default",
|
||||
"start_time": "2025-08-12T14:54:26.221804768+08:00",
|
||||
"status": 2,
|
||||
"total": 1,
|
||||
"finished": 1
|
||||
},
|
||||
"params": {
|
||||
"session_id": "",
|
||||
"knowledge_base_id": "2ef57434-8c8d-4442-b967-2f7fc578a2fc",
|
||||
"vector_threshold": 0.5,
|
||||
"keyword_threshold": 0.3,
|
||||
"embedding_top_k": 10,
|
||||
"vector_database": "",
|
||||
"rerank_model_id": "b30171a1-787b-426e-a293-735cd5ac16c0",
|
||||
"rerank_top_k": 5,
|
||||
"rerank_threshold": 0.7,
|
||||
"chat_model_id": "8aea788c-bb30-4898-809e-e40c14ffb48c",
|
||||
"summary_config": {
|
||||
"max_tokens": 0,
|
||||
"repeat_penalty": 1,
|
||||
"top_k": 0,
|
||||
"top_p": 0,
|
||||
"frequency_penalty": 0,
|
||||
"presence_penalty": 0,
|
||||
"prompt": "这是用户和助手之间的对话。",
|
||||
"context_template": "你是一个专业的智能信息检索助手",
|
||||
"no_match_prefix": "<think>\n</think>\nNO_MATCH",
|
||||
"temperature": 0.3,
|
||||
"seed": 0,
|
||||
"max_completion_tokens": 2048
|
||||
},
|
||||
"fallback_strategy": "",
|
||||
"fallback_response": "抱歉,我无法回答这个问题。"
|
||||
},
|
||||
"metric": {
|
||||
"retrieval_metrics": {
|
||||
"precision": 0,
|
||||
"recall": 0,
|
||||
"ndcg3": 0,
|
||||
"ndcg10": 0,
|
||||
"mrr": 0,
|
||||
"map": 0
|
||||
},
|
||||
"generation_metrics": {
|
||||
"bleu1": 0.037656734016532384,
|
||||
"bleu2": 0.04067392145167686,
|
||||
"bleu4": 0.048963321289052536,
|
||||
"rouge1": 0,
|
||||
"rouge2": 0,
|
||||
"rougel": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/evaluation` - 创建评估任务
|
||||
|
||||
**参数说明(请求体)**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ----------------------------------------------- |
|
||||
| dataset_id | string | 是 | 评估数据集,目前仅支持 `default`(官方测试集) |
|
||||
| knowledge_base_id | string | 是 | 评估使用的知识库 ID |
|
||||
| chat_id | string | 是 | 评估使用的对话模型 ID |
|
||||
| rerank_id | string | 是 | 评估使用的重排序模型 ID |
|
||||
|
||||
**请求**:
|
||||
|
||||
```bash
|
||||
curl --location 'http://localhost:8080/api/v1/evaluation' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"dataset_id": "default",
|
||||
"knowledge_base_id": "kb-00000001",
|
||||
"chat_id": "8aea788c-bb30-4898-809e-e40c14ffb48c",
|
||||
"rerank_id": "b30171a1-787b-426e-a293-735cd5ac16c0"
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"task": {
|
||||
"id": "c34563ad-b09f-4858-b72e-e92beb80becb",
|
||||
"tenant_id": 1,
|
||||
"dataset_id": "default",
|
||||
"start_time": "2025-08-12T14:54:26.221804768+08:00",
|
||||
"status": 1
|
||||
},
|
||||
"params": {
|
||||
"session_id": "",
|
||||
"knowledge_base_id": "2ef57434-8c8d-4442-b967-2f7fc578a2fc",
|
||||
"vector_threshold": 0.5,
|
||||
"keyword_threshold": 0.3,
|
||||
"embedding_top_k": 10,
|
||||
"vector_database": "",
|
||||
"rerank_model_id": "b30171a1-787b-426e-a293-735cd5ac16c0",
|
||||
"rerank_top_k": 5,
|
||||
"rerank_threshold": 0.7,
|
||||
"chat_model_id": "8aea788c-bb30-4898-809e-e40c14ffb48c",
|
||||
"summary_config": {
|
||||
"max_tokens": 0,
|
||||
"repeat_penalty": 1,
|
||||
"top_k": 0,
|
||||
"top_p": 0,
|
||||
"frequency_penalty": 0,
|
||||
"presence_penalty": 0,
|
||||
"prompt": "这是用户和助手之间的对话。",
|
||||
"context_template": "你是一个专业的智能信息检索助手,xxx",
|
||||
"no_match_prefix": "<think>\n</think>\nNO_MATCH",
|
||||
"temperature": 0.3,
|
||||
"seed": 0,
|
||||
"max_completion_tokens": 2048
|
||||
},
|
||||
"fallback_strategy": "",
|
||||
"fallback_response": "抱歉,我无法回答这个问题。"
|
||||
}
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,562 @@
|
||||
# FAQ管理 API
|
||||
|
||||
[返回目录](./README.md)
|
||||
|
||||
FAQ 接口分为两组:
|
||||
|
||||
- `/knowledge-bases/:id/faq/*`:知识库范围内的 FAQ 条目 CRUD、批量操作、搜索与导入导出。
|
||||
- `/faq/import/progress/:task_id`:**不属于知识库分组**,用于查询异步导入/dry-run 任务的进度,仅需任务 ID 即可调用。
|
||||
|
||||
| 方法 | 路径 | 描述 |
|
||||
| ------ | ----------------------------------------------------------------- | --------------------------------- |
|
||||
| GET | `/knowledge-bases/:id/faq/entries` | 获取 FAQ 条目列表 |
|
||||
| GET | `/knowledge-bases/:id/faq/entries/export` | 导出 FAQ 条目(CSV) |
|
||||
| GET | `/knowledge-bases/:id/faq/entries/:entry_id` | 获取单个 FAQ 条目(按 seq_id) |
|
||||
| POST | `/knowledge-bases/:id/faq/entries` | 批量 Upsert FAQ 条目(异步) |
|
||||
| POST | `/knowledge-bases/:id/faq/entry` | 同步创建单个 FAQ 条目 |
|
||||
| PUT | `/knowledge-bases/:id/faq/entries/:entry_id` | 更新单个 FAQ 条目 |
|
||||
| POST | `/knowledge-bases/:id/faq/entries/:entry_id/similar-questions` | 为 FAQ 条目追加相似问 |
|
||||
| PUT | `/knowledge-bases/:id/faq/entries/fields` | 批量更新字段(启用/推荐/标签) |
|
||||
| PUT | `/knowledge-bases/:id/faq/entries/tags` | 批量更新标签 |
|
||||
| DELETE | `/knowledge-bases/:id/faq/entries` | 批量删除 FAQ 条目 |
|
||||
| POST | `/knowledge-bases/:id/faq/search` | FAQ 混合搜索 |
|
||||
| PUT | `/knowledge-bases/:id/faq/import/last-result/display` | 更新上次导入结果卡片显示状态 |
|
||||
| GET | `/faq/import/progress/:task_id` | 查询 FAQ 导入任务进度(公共) |
|
||||
|
||||
> **路径参数说明**:`:entry_id` 始终是 FAQ 条目的 `seq_id`(整数),不是字符串形式的 ID。同理,批量接口中的 `by_id` / `by_tag` / `exclude_ids` / `ids` 字段均为 `seq_id` 列表(整数)。
|
||||
|
||||
## GET `/knowledge-bases/:id/faq/entries` - 获取 FAQ 条目列表
|
||||
|
||||
支持分页、按标签过滤、关键字搜索与排序。
|
||||
|
||||
**查询参数**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ------------ | ------ | ---- | --------------------------------------------------------------------------------------------- |
|
||||
| page | int | 否 | 页码,默认 1 |
|
||||
| page_size | int | 否 | 每页数量,默认 20 |
|
||||
| tag_id | int | 否 | 按标签 `seq_id` 过滤 |
|
||||
| keyword | string | 否 | 关键字搜索 |
|
||||
| search_field | string | 否 | 搜索字段:`standard_question` / `similar_questions` / `answers`,留空则全字段搜索 |
|
||||
| sort_order | string | 否 | 排序方式,`asc` 表示按更新时间正序,默认按更新时间倒序 |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
# 全字段搜索
|
||||
curl --location 'http://localhost:8080/api/v1/knowledge-bases/kb-00000001/faq/entries?page=1&page_size=10&keyword=密码' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
|
||||
# 仅搜索标准问
|
||||
curl --location 'http://localhost:8080/api/v1/knowledge-bases/kb-00000001/faq/entries?keyword=密码&search_field=standard_question' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"total": 100,
|
||||
"page": 1,
|
||||
"page_size": 10,
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"chunk_id": "chunk-00000001",
|
||||
"knowledge_id": "knowledge-00000001",
|
||||
"knowledge_base_id": "kb-00000001",
|
||||
"tag_id": 12,
|
||||
"tag_name": "账户",
|
||||
"is_enabled": true,
|
||||
"is_recommended": false,
|
||||
"standard_question": "如何重置密码?",
|
||||
"similar_questions": ["忘记密码怎么办", "密码找回"],
|
||||
"negative_questions": ["如何修改用户名"],
|
||||
"answers": ["您可以通过点击登录页面的'忘记密码'链接来重置密码。"],
|
||||
"answer_strategy": "all",
|
||||
"index_mode": "hybrid",
|
||||
"chunk_type": "faq",
|
||||
"created_at": "2025-08-12T10:00:00+08:00",
|
||||
"updated_at": "2025-08-12T10:00:00+08:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## GET `/knowledge-bases/:id/faq/entries/export` - 导出 FAQ 条目
|
||||
|
||||
将知识库下的所有 FAQ 条目导出为 CSV(UTF-8 带 BOM,Excel 兼容)。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/knowledge-bases/kb-00000001/faq/entries/export' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--output faq_export.csv
|
||||
```
|
||||
|
||||
**响应**: `Content-Type: text/csv; charset=utf-8`,附带文件名 `faq_export.csv`。
|
||||
|
||||
## GET `/knowledge-bases/:id/faq/entries/:entry_id` - 获取单个 FAQ 条目
|
||||
|
||||
根据 `seq_id`(整数)获取单个 FAQ 条目详情。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/knowledge-bases/kb-00000001/faq/entries/1' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": 1,
|
||||
"chunk_id": "chunk-00000001",
|
||||
"knowledge_id": "knowledge-00000001",
|
||||
"knowledge_base_id": "kb-00000001",
|
||||
"tag_id": 12,
|
||||
"tag_name": "账户",
|
||||
"is_enabled": true,
|
||||
"is_recommended": false,
|
||||
"standard_question": "如何重置密码?",
|
||||
"similar_questions": ["忘记密码怎么办", "密码找回"],
|
||||
"negative_questions": [],
|
||||
"answers": ["您可以通过点击登录页面的'忘记密码'链接来重置密码。"],
|
||||
"answer_strategy": "all",
|
||||
"index_mode": "hybrid",
|
||||
"chunk_type": "faq",
|
||||
"created_at": "2025-08-12T10:00:00+08:00",
|
||||
"updated_at": "2025-08-12T10:00:00+08:00"
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/knowledge-bases/:id/faq/entries` - 批量 Upsert FAQ 条目(异步)
|
||||
|
||||
**异步**批量导入或更新 FAQ 条目。接口立即返回 `task_id`,调用方需通过 `GET /faq/import/progress/:task_id` 查询进度与结果。
|
||||
|
||||
支持 `dry_run=true`:异步执行仅校验(格式 / 批内重复 / 与库内重复 / 内容安全),不实际写入。
|
||||
|
||||
**请求体(`types.FAQBatchUpsertPayload`)**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ------------ | -------------------------- | ---- | ----------------------------------------------------------------------------------- |
|
||||
| entries | `[]FAQEntryPayload` | 是 | FAQ 条目数组 |
|
||||
| mode | string | 是 | `append` 或 `replace`(替换会清空已有条目) |
|
||||
| knowledge_id | string | 否 | 关联的 FAQ Knowledge ID(不传则使用知识库默认 FAQ knowledge) |
|
||||
| task_id | string | 否 | 任务 ID,不传则自动生成 UUID |
|
||||
| dry_run | boolean | 否 | 仅验证不导入 |
|
||||
|
||||
`FAQEntryPayload` 字段:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ------------------- | --------- | ---- | ------------------------------------------------------------- |
|
||||
| id | int64 | 否 | 指定 `seq_id`(数据迁移场景,需小于自增起始值 100000000) |
|
||||
| standard_question | string | 是 | 标准问 |
|
||||
| similar_questions | string[] | 否 | 相似问列表 |
|
||||
| negative_questions | string[] | 否 | 反例问题列表 |
|
||||
| answers | string[] | 否 | 答案列表 |
|
||||
| answer_strategy | string | 否 | 答案返回策略:`all` 或 `random` |
|
||||
| tag_id | int64 | 否 | 标签 `seq_id` |
|
||||
| tag_name | string | 否 | 标签名(用于按名匹配标签) |
|
||||
| is_enabled | boolean | 否 | 是否启用 |
|
||||
| is_recommended | boolean | 否 | 是否推荐 |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/knowledge-bases/kb-00000001/faq/entries' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"mode": "append",
|
||||
"entries": [
|
||||
{
|
||||
"standard_question": "如何联系客服?",
|
||||
"similar_questions": ["客服电话", "在线客服"],
|
||||
"answers": ["您可以通过拨打400-xxx-xxxx联系我们的客服。"],
|
||||
"tag_id": 1
|
||||
},
|
||||
{
|
||||
"standard_question": "退款政策是什么?",
|
||||
"answers": ["我们提供7天无理由退款服务。"]
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": { "task_id": "task-00000001" },
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
> 用 `GET /faq/import/progress/:task_id` 查询任务最终状态。
|
||||
|
||||
## POST `/knowledge-bases/:id/faq/entry` - 同步创建单个 FAQ 条目
|
||||
|
||||
**同步**创建单条 FAQ 条目,会即时校验标准问/相似问与库内已有条目的重复。
|
||||
|
||||
**请求体**: 同上 `FAQEntryPayload`(`standard_question` 必填)。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/knowledge-bases/kb-00000001/faq/entry' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"standard_question": "如何联系客服?",
|
||||
"similar_questions": ["客服电话", "在线客服"],
|
||||
"answers": ["您可以通过拨打400-xxx-xxxx联系我们的客服。"],
|
||||
"tag_id": 1,
|
||||
"is_enabled": true
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": 1,
|
||||
"chunk_id": "chunk-00000001",
|
||||
"knowledge_id": "knowledge-00000001",
|
||||
"knowledge_base_id": "kb-00000001",
|
||||
"tag_id": 1,
|
||||
"tag_name": "客服",
|
||||
"is_enabled": true,
|
||||
"is_recommended": false,
|
||||
"standard_question": "如何联系客服?",
|
||||
"similar_questions": ["客服电话", "在线客服"],
|
||||
"negative_questions": [],
|
||||
"answers": ["您可以通过拨打400-xxx-xxxx联系我们的客服。"],
|
||||
"answer_strategy": "all",
|
||||
"index_mode": "hybrid",
|
||||
"chunk_type": "faq",
|
||||
"created_at": "2025-08-12T10:00:00+08:00",
|
||||
"updated_at": "2025-08-12T10:00:00+08:00"
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**(标准问或相似问重复时):
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "BAD_REQUEST",
|
||||
"message": "标准问与已有FAQ重复"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## PUT `/knowledge-bases/:id/faq/entries/:entry_id` - 更新单个 FAQ 条目
|
||||
|
||||
按 `seq_id` 更新单条 FAQ 条目,请求体同 `FAQEntryPayload`。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request PUT 'http://localhost:8080/api/v1/knowledge-bases/kb-00000001/faq/entries/1' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"standard_question": "如何重置账户密码?",
|
||||
"similar_questions": ["忘记密码怎么办", "密码找回", "重置密码"],
|
||||
"answers": ["您可以通过以下步骤重置密码:1. 点击登录页面的\"忘记密码\" 2. 输入注册邮箱 3. 查收重置邮件"],
|
||||
"is_enabled": true
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**: 返回更新后的 FAQ 条目,结构同创建接口。
|
||||
|
||||
## POST `/knowledge-bases/:id/faq/entries/:entry_id/similar-questions` - 追加相似问
|
||||
|
||||
向指定 FAQ 条目(`seq_id`)追加相似问。
|
||||
|
||||
**请求体**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | -------- | ---- | --------------------- |
|
||||
| similar_questions | string[] | 是 | 要追加的相似问数组 |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/knowledge-bases/kb-00000001/faq/entries/1/similar-questions' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"similar_questions": ["怎样修改密码", "密码重置方法"]
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**: 返回追加后的完整 FAQ 条目。
|
||||
|
||||
## PUT `/knowledge-bases/:id/faq/entries/fields` - 批量更新字段
|
||||
|
||||
**统一**的批量字段更新接口,支持同时更新 `is_enabled` / `is_recommended` / `tag_id`,并支持两种作用域:
|
||||
|
||||
- **按条目 ID** (`by_id`):键为条目 `seq_id`,值为该条目要更新的字段。
|
||||
- **按标签 ID** (`by_tag`):键为标签 `seq_id`,对该标签下的所有条目应用相同的字段更新;可配合 `exclude_ids` 排除部分条目。
|
||||
|
||||
`by_id` 和 `by_tag` 至少传一项;二者可同时使用。
|
||||
|
||||
**请求体(`types.FAQEntryFieldsBatchUpdate`)**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ----------- | --------------------------------- | ---- | ------------------------------------------ |
|
||||
| by_id | `map[int64]FAQEntryFieldsUpdate` | 否 | 按条目 `seq_id` 更新 |
|
||||
| by_tag | `map[int64]FAQEntryFieldsUpdate` | 否 | 按标签 `seq_id` 对该标签下所有条目更新 |
|
||||
| exclude_ids | `int64[]` | 否 | 与 `by_tag` 配合使用,排除指定条目 `seq_id` |
|
||||
|
||||
`FAQEntryFieldsUpdate` 字段(全部可选,仅传入的字段会被更新):
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| -------------- | ------- | -------------- |
|
||||
| is_enabled | boolean | 是否启用 |
|
||||
| is_recommended | boolean | 是否推荐 |
|
||||
| tag_id | int64 | 标签 `seq_id` |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request PUT 'http://localhost:8080/api/v1/knowledge-bases/kb-00000001/faq/entries/fields' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"by_id": {
|
||||
"1": {"is_enabled": true, "is_recommended": false},
|
||||
"2": {"is_enabled": false}
|
||||
},
|
||||
"by_tag": {
|
||||
"100": {"is_recommended": true}
|
||||
},
|
||||
"exclude_ids": [3, 4]
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{ "success": true }
|
||||
```
|
||||
|
||||
## PUT `/knowledge-bases/:id/faq/entries/tags` - 批量更新标签
|
||||
|
||||
仅更新标签关联。键为条目 `seq_id`,值为目标标签 `seq_id`;值传 `null` 表示清除标签。
|
||||
|
||||
**请求体**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ------- | --------------------- | ---- | -------------------------------------------- |
|
||||
| updates | `map[int64]int64?` | 是 | 键:条目 `seq_id`;值:标签 `seq_id` 或 `null` |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request PUT 'http://localhost:8080/api/v1/knowledge-bases/kb-00000001/faq/entries/tags' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"updates": {
|
||||
"1": 10,
|
||||
"2": 11,
|
||||
"3": null
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{ "success": true }
|
||||
```
|
||||
|
||||
## DELETE `/knowledge-bases/:id/faq/entries` - 批量删除
|
||||
|
||||
**请求体**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ---- | --------- | ---- | --------------------------------- |
|
||||
| ids | `int64[]` | 是 | 要删除的 FAQ 条目 `seq_id` 列表 |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request DELETE 'http://localhost:8080/api/v1/knowledge-bases/kb-00000001/faq/entries' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"ids": [1, 2, 3]
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{ "success": true }
|
||||
```
|
||||
|
||||
## POST `/knowledge-bases/:id/faq/search` - FAQ 混合搜索
|
||||
|
||||
向量 + 关键字混合检索,支持两级优先级标签召回。
|
||||
|
||||
**请求体(`types.FAQSearchRequest`)**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ----------------------- | --------- | ---- | ----------------------------------------------------------------------------------- |
|
||||
| query_text | string | 是 | 搜索文本 |
|
||||
| vector_threshold | float | 否 | 向量相似度阈值(0–1) |
|
||||
| match_count | int | 否 | 返回数量,默认 10,最大 200 |
|
||||
| first_priority_tag_ids | `int64[]` | 否 | 第一优先级标签 `seq_id` 列表(最高优先召回范围) |
|
||||
| second_priority_tag_ids | `int64[]` | 否 | 第二优先级标签 `seq_id` 列表 |
|
||||
| only_recommended | boolean | 否 | 是否仅返回 `is_recommended=true` 的条目 |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/knowledge-bases/kb-00000001/faq/search' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"query_text": "如何重置密码",
|
||||
"vector_threshold": 0.5,
|
||||
"match_count": 10,
|
||||
"first_priority_tag_ids": [12],
|
||||
"only_recommended": false
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"chunk_id": "chunk-00000001",
|
||||
"knowledge_id": "knowledge-00000001",
|
||||
"knowledge_base_id": "kb-00000001",
|
||||
"tag_id": 12,
|
||||
"tag_name": "账户",
|
||||
"is_enabled": true,
|
||||
"is_recommended": false,
|
||||
"standard_question": "如何重置密码?",
|
||||
"similar_questions": ["忘记密码怎么办", "密码找回"],
|
||||
"answers": ["您可以通过点击登录页面的'忘记密码'链接来重置密码。"],
|
||||
"answer_strategy": "all",
|
||||
"chunk_type": "faq",
|
||||
"score": 0.95,
|
||||
"match_type": "vector",
|
||||
"matched_question": "忘记密码怎么办",
|
||||
"created_at": "2025-08-12T10:00:00+08:00",
|
||||
"updated_at": "2025-08-12T10:00:00+08:00"
|
||||
}
|
||||
],
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## PUT `/knowledge-bases/:id/faq/import/last-result/display` - 更新上次导入结果显示状态
|
||||
|
||||
控制上次导入完成后,前端结果卡片的显示/隐藏。
|
||||
|
||||
**请求体**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| -------------- | ------ | ---- | --------------------- |
|
||||
| display_status | string | 是 | `open` 或 `close` |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request PUT 'http://localhost:8080/api/v1/knowledge-bases/kb-00000001/faq/import/last-result/display' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"display_status": "close"
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{ "success": true }
|
||||
```
|
||||
|
||||
## GET `/faq/import/progress/:task_id` - 查询 FAQ 导入进度
|
||||
|
||||
> **注意**:此接口**不在** `/knowledge-bases/:id/faq` 分组下,路径直接以 `/faq/import/progress/:task_id` 开头。任务 ID 由 `POST /knowledge-bases/:id/faq/entries` 返回。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| ------- | ------ | -------------- |
|
||||
| task_id | string | 导入任务的 ID |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/faq/import/progress/task-00000001' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应**(节选关键字段):
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"task_id": "task-00000001",
|
||||
"kb_id": "kb-00000001",
|
||||
"knowledge_id": "knowledge-00000001",
|
||||
"status": "completed",
|
||||
"progress": 100,
|
||||
"total": 100,
|
||||
"processed": 100,
|
||||
"success_count": 95,
|
||||
"failed_count": 3,
|
||||
"partial_failed_count": 2,
|
||||
"skipped_count": 0,
|
||||
"failed_entries": [
|
||||
{
|
||||
"index": 5,
|
||||
"reason": "标准问与已有FAQ重复",
|
||||
"standard_question": "重复的问题"
|
||||
}
|
||||
],
|
||||
"success_entries": [
|
||||
{ "index": 0, "seq_id": 101, "standard_question": "如何联系客服?" }
|
||||
],
|
||||
"message": "",
|
||||
"error": "",
|
||||
"created_at": 1736582400,
|
||||
"updated_at": 1736582460,
|
||||
"dry_run": false,
|
||||
"import_mode": "append",
|
||||
"imported_at": "2025-08-12T10:01:00+08:00",
|
||||
"display_status": "open",
|
||||
"processing_time": 60000
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
`status` 可能取值:`pending` / `processing` / `completed` / `failed`。
|
||||
|
||||
当失败条目过多时,`failed_entries` 可能不直接返回,而通过 `failed_entries_url` 提供 CSV 下载地址。
|
||||
|
||||
`dry_run=true` 模式下的任务同样通过此接口查询,`success_entries` 中的 `seq_id` 不会真正写入。
|
||||
@@ -0,0 +1,402 @@
|
||||
# 初始化配置 API
|
||||
|
||||
[返回目录](./README.md)
|
||||
|
||||
| 方法 | 路径 | 描述 |
|
||||
| ------ | ------------------------------------------------- | -------------------------- |
|
||||
| GET | `/initialization/config/:kb_id` | 获取知识库初始化配置 |
|
||||
| POST | `/initialization/initialize/:kb_id` | 初始化知识库模型配置 |
|
||||
| PUT | `/initialization/config/:kb_id` | 更新知识库模型配置 |
|
||||
| GET | `/initialization/ollama/status` | 检查 Ollama 状态 |
|
||||
| GET | `/initialization/ollama/models` | 获取本地 Ollama 模型列表 |
|
||||
| POST | `/initialization/ollama/models/check` | 检查 Ollama 模型是否可用 |
|
||||
| POST | `/initialization/ollama/models/download` | 下载 Ollama 模型 |
|
||||
| GET | `/initialization/ollama/download/progress/:task_id` | 获取下载进度 |
|
||||
| GET | `/initialization/ollama/download/tasks` | 获取所有下载任务 |
|
||||
| POST | `/initialization/remote/check` | 检查远程模型 API |
|
||||
| POST | `/initialization/embedding/test` | 测试嵌入模型 |
|
||||
| POST | `/initialization/rerank/check` | 检查重排序模型 |
|
||||
| POST | `/initialization/multimodal/test` | 测试多模态模型 |
|
||||
| POST | `/initialization/extract/text-relation` | 提取文本关系 |
|
||||
|
||||
## GET `/initialization/config/:kb_id` - 获取知识库初始化配置
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/initialization/config/kb-00000001' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"chat_model_id": "model-00000001",
|
||||
"embedding_model_id": "model-00000002",
|
||||
"rerank_model_id": "model-00000003",
|
||||
"multimodal_id": "model-00000004"
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/initialization/initialize/:kb_id` - 初始化知识库模型配置
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/initialization/initialize/kb-00000001' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"chat_model_id": "model-00000001",
|
||||
"embedding_model_id": "model-00000002",
|
||||
"rerank_model_id": "model-00000003",
|
||||
"multimodal_id": "model-00000004"
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## PUT `/initialization/config/:kb_id` - 更新知识库模型配置
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request PUT 'http://localhost:8080/api/v1/initialization/config/kb-00000001' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"chat_model_id": "model-00000010",
|
||||
"embedding_model_id": "model-00000002"
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## GET `/initialization/ollama/status` - 检查 Ollama 状态
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/initialization/ollama/status' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"available": true
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## GET `/initialization/ollama/models` - 获取本地 Ollama 模型列表
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/initialization/ollama/models' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"name": "llama3:8b",
|
||||
"size": 4661211648,
|
||||
"modified_at": "2025-08-10T15:30:00+08:00"
|
||||
},
|
||||
{
|
||||
"name": "nomic-embed-text:latest",
|
||||
"size": 274302976,
|
||||
"modified_at": "2025-08-11T09:00:00+08:00"
|
||||
}
|
||||
],
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/initialization/ollama/models/check` - 检查 Ollama 模型是否可用
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/initialization/ollama/models/check' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"models": ["llama3:8b", "nomic-embed-text:latest", "mistral:7b"]
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"llama3:8b": true,
|
||||
"nomic-embed-text:latest": true,
|
||||
"mistral:7b": false
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/initialization/ollama/models/download` - 下载 Ollama 模型
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/initialization/ollama/models/download' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"model": "mistral:7b"
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "task-00000001",
|
||||
"modelName": "mistral:7b",
|
||||
"status": "downloading",
|
||||
"progress": 0,
|
||||
"message": "开始下载",
|
||||
"startTime": "2025-08-12T10:00:00+08:00"
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## GET `/initialization/ollama/download/progress/:task_id` - 获取下载进度
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/initialization/ollama/download/progress/task-00000001' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "task-00000001",
|
||||
"modelName": "mistral:7b",
|
||||
"status": "downloading",
|
||||
"progress": 45.6,
|
||||
"message": "正在下载 2.1GB / 4.6GB",
|
||||
"startTime": "2025-08-12T10:00:00+08:00"
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## GET `/initialization/ollama/download/tasks` - 获取所有下载任务
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/initialization/ollama/download/tasks' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "task-00000001",
|
||||
"modelName": "mistral:7b",
|
||||
"status": "completed",
|
||||
"progress": 100,
|
||||
"message": "下载完成",
|
||||
"startTime": "2025-08-12T10:00:00+08:00",
|
||||
"endTime": "2025-08-12T10:15:00+08:00"
|
||||
},
|
||||
{
|
||||
"id": "task-00000002",
|
||||
"modelName": "llama3:70b",
|
||||
"status": "downloading",
|
||||
"progress": 30.2,
|
||||
"message": "正在下载 12.5GB / 41.4GB",
|
||||
"startTime": "2025-08-12T10:20:00+08:00"
|
||||
}
|
||||
],
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/initialization/remote/check` - 检查远程模型 API
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/initialization/remote/check' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"api_url": "https://api.openai.com/v1",
|
||||
"api_key": "sk-xxxxx",
|
||||
"model": "gpt-4o"
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"success": true,
|
||||
"message": "模型可用"
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/initialization/embedding/test` - 测试嵌入模型
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/initialization/embedding/test' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"api_url": "https://api.openai.com/v1",
|
||||
"api_key": "sk-xxxxx",
|
||||
"model": "text-embedding-3-small"
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"success": true,
|
||||
"message": "嵌入模型测试通过"
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/initialization/rerank/check` - 检查重排序模型
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/initialization/rerank/check' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"api_url": "https://api.cohere.ai/v1",
|
||||
"api_key": "sk-xxxxx",
|
||||
"model": "rerank-english-v3.0"
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"success": true,
|
||||
"message": "重排序模型可用"
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/initialization/multimodal/test` - 测试多模态模型
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/initialization/multimodal/test' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"api_url": "https://api.openai.com/v1",
|
||||
"api_key": "sk-xxxxx",
|
||||
"model": "gpt-4o"
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"success": true,
|
||||
"message": "多模态模型测试通过"
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/initialization/extract/text-relation` - 提取文本关系
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/initialization/extract/text-relation' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"text": "WeKnora 是一个知识管理平台,支持多种文档格式的解析和检索。",
|
||||
"model_id": "model-00000001"
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"entities": [
|
||||
{"name": "WeKnora", "type": "Product"},
|
||||
{"name": "知识管理平台", "type": "Concept"}
|
||||
],
|
||||
"relations": [
|
||||
{
|
||||
"source": "WeKnora",
|
||||
"target": "知识管理平台",
|
||||
"relation": "is_a"
|
||||
}
|
||||
]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,722 @@
|
||||
# 知识库管理 API
|
||||
|
||||
[返回目录](./README.md)
|
||||
|
||||
**字段说明(知识库对象)**
|
||||
|
||||
- 知识库类型 `type` 为 `document`(文档)或 `faq`(FAQ),默认 `document`。
|
||||
- JSON 中对象存储相关字段:**`storage_config`** 为序列化字段名(对应数据库列 `cos_config`,兼容旧数据)。旧客户端若仍发送或接收 `cos_config`,服务端会兼容解析;新集成请使用 **`storage_config`**。
|
||||
- **`storage_provider_config`** 为新版存储提供者选择(如 `{"provider": "local"}`),与空间级存储引擎凭证配合使用;无配置时可为 `null`。
|
||||
- 嵌套配置对象:`chunking_config`、`image_processing_config`、`vlm_config`、`asr_config`、`extract_config`、`faq_config`、`question_generation_config`、`auto_tag_config`。其中 `extract_config`、`faq_config`、`question_generation_config`、`auto_tag_config` 允许为 `null`。
|
||||
- **`vector_store_id`** 为知识库绑定的向量存储 ID(参见 [vector-store.md](./vector-store.md))。未指定(或 `null`/`""`)时使用空间级默认的环境变量存储;一旦创建即不可修改。详情接口返回时会附带 `vector_store_name` / `vector_store_source` / `vector_store_engine_type` / `vector_store_status` 四个只读元数据字段,用于前端展示。
|
||||
|
||||
| 方法 | 路径 | 描述 |
|
||||
| ------ | ----------------------------------------- | ------------------------ |
|
||||
| POST | `/knowledge-bases` | 创建知识库 |
|
||||
| GET | `/knowledge-bases` | 获取知识库列表 |
|
||||
| GET | `/knowledge-bases/:id` | 获取知识库详情 |
|
||||
| PUT | `/knowledge-bases/:id` | 更新知识库 |
|
||||
| DELETE | `/knowledge-bases/:id` | 删除知识库 |
|
||||
| PUT | `/knowledge-bases/:id/pin` | 置顶/取消置顶知识库 |
|
||||
| POST | `/knowledge-bases/:id/hybrid-search` | 混合搜索(向量+关键词,推荐) |
|
||||
| GET | `/knowledge-bases/:id/hybrid-search` | 混合搜索(兼容旧客户端,需 JSON 请求体) |
|
||||
| POST | `/knowledge-bases/copy` | 拷贝知识库(异步任务) |
|
||||
| GET | `/knowledge-bases/copy/progress/:task_id` | 获取拷贝进度 |
|
||||
| POST | `/knowledge-bases/:id/duplicate` | 创建知识库副本(仅设置) |
|
||||
| GET | `/knowledge-bases/:id/move-targets` | 获取可迁移目标知识库列表 |
|
||||
|
||||
## POST `/knowledge-bases` - 创建知识库
|
||||
|
||||
**参数说明(请求体)**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ----------------------------- | ------- | ---- | --------------------------------------------------------------- |
|
||||
| name | string | 是 | 知识库名称 |
|
||||
| description | string | 否 | 知识库描述 |
|
||||
| type | string | 否 | 知识库类型:`document`(默认)或 `faq` |
|
||||
| is_temporary | boolean | 否 | 是否为临时知识库(默认 `false`,临时库通常不在 UI 列表中显示) |
|
||||
| chunking_config | object | 否 | 分块配置(见下方示例) |
|
||||
| image_processing_config | object | 否 | 图片处理配置 |
|
||||
| embedding_model_id | string | 否 | Embedding 模型 ID |
|
||||
| summary_model_id | string | 否 | 摘要模型 ID |
|
||||
| vlm_config | object | 否 | VLM(视觉模型)配置 |
|
||||
| asr_config | object | 否 | ASR(语音识别)配置 |
|
||||
| storage_provider_config | object | 否 | 存储提供者选择,如 `{"provider": "local"}` |
|
||||
| storage_config | object | 否 | 旧版 COS 存储凭证(兼容字段,新集成留空即可) |
|
||||
| extract_config | object | 否 | 图谱抽取配置;`enabled=true` 时需提供 `text`/`tags`/`nodes`/`relations` |
|
||||
| faq_config | object | 否 | FAQ 配置(仅 FAQ 类型知识库需要) |
|
||||
| question_generation_config | object | 否 | 问题生成配置 |
|
||||
| auto_tag_config | object | 否 | 文档自动标签配置,默认关闭;仅适用于 `document` 类型知识库 |
|
||||
| vector_store_id | string | 否 | 绑定的向量存储 ID。不传或为空字符串等同于 `null`(使用环境变量默认存储)。指定时必须是调用者所在空间拥有的向量存储 UUID;创建后不可修改。无效 UUID / 跨空间 / 未注册到引擎的 ID 会返回 `400` |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/knowledge-bases' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--data '{
|
||||
"name": "weknora",
|
||||
"description": "weknora description",
|
||||
"type": "document",
|
||||
"is_temporary": false,
|
||||
"chunking_config": {
|
||||
"chunk_size": 1000,
|
||||
"chunk_overlap": 200,
|
||||
"separators": [
|
||||
"."
|
||||
],
|
||||
"enable_multimodal": true,
|
||||
"parser_engine_rules": [
|
||||
{
|
||||
"file_types": [".pdf", ".docx"],
|
||||
"engine": "builtin"
|
||||
}
|
||||
],
|
||||
"enable_parent_child": false,
|
||||
"parent_chunk_size": 4096,
|
||||
"child_chunk_size": 384
|
||||
},
|
||||
"image_processing_config": {
|
||||
"model_id": "f2083ad7-63e3-486d-a610-e6c56e58d72e"
|
||||
},
|
||||
"embedding_model_id": "dff7bc94-7885-4dd1-bfd5-bd96e4df2fc3",
|
||||
"summary_model_id": "8aea788c-bb30-4898-809e-e40c14ffb48c",
|
||||
"vlm_config": {
|
||||
"enabled": true,
|
||||
"model_id": "f2083ad7-63e3-486d-a610-e6c56e58d72e"
|
||||
},
|
||||
"asr_config": {
|
||||
"enabled": false,
|
||||
"model_id": "",
|
||||
"language": ""
|
||||
},
|
||||
"storage_provider_config": {
|
||||
"provider": "local"
|
||||
},
|
||||
"storage_config": {
|
||||
"secret_id": "",
|
||||
"secret_key": "",
|
||||
"region": "",
|
||||
"bucket_name": "",
|
||||
"app_id": "",
|
||||
"path_prefix": ""
|
||||
},
|
||||
"extract_config": null,
|
||||
"faq_config": null,
|
||||
"question_generation_config": {
|
||||
"enabled": false,
|
||||
"question_count": 3
|
||||
},
|
||||
"auto_tag_config": {
|
||||
"enabled": true,
|
||||
"model_id": "8aea788c-bb30-4898-809e-e40c14ffb48c",
|
||||
"max_tags": 3,
|
||||
"skip_if_tagged": true
|
||||
},
|
||||
"vector_store_id": "550e8400-e29b-41d4-a716-446655440000"
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "b5829e4a-3845-4624-a7fb-ea3b35e843b0",
|
||||
"name": "weknora",
|
||||
"description": "weknora description",
|
||||
"type": "document",
|
||||
"is_temporary": false,
|
||||
"tenant_id": 1,
|
||||
"chunking_config": {
|
||||
"chunk_size": 1000,
|
||||
"chunk_overlap": 200,
|
||||
"separators": [
|
||||
"."
|
||||
],
|
||||
"enable_multimodal": true,
|
||||
"parser_engine_rules": [
|
||||
{
|
||||
"file_types": [".pdf", ".docx"],
|
||||
"engine": "builtin"
|
||||
}
|
||||
],
|
||||
"enable_parent_child": false,
|
||||
"parent_chunk_size": 4096,
|
||||
"child_chunk_size": 384
|
||||
},
|
||||
"image_processing_config": {
|
||||
"model_id": "f2083ad7-63e3-486d-a610-e6c56e58d72e"
|
||||
},
|
||||
"embedding_model_id": "dff7bc94-7885-4dd1-bfd5-bd96e4df2fc3",
|
||||
"summary_model_id": "8aea788c-bb30-4898-809e-e40c14ffb48c",
|
||||
"vlm_config": {
|
||||
"enabled": true,
|
||||
"model_id": "f2083ad7-63e3-486d-a610-e6c56e58d72e"
|
||||
},
|
||||
"asr_config": {
|
||||
"enabled": false,
|
||||
"model_id": "",
|
||||
"language": ""
|
||||
},
|
||||
"storage_provider_config": {
|
||||
"provider": "local"
|
||||
},
|
||||
"storage_config": {
|
||||
"secret_id": "",
|
||||
"secret_key": "",
|
||||
"region": "",
|
||||
"bucket_name": "",
|
||||
"app_id": "",
|
||||
"path_prefix": ""
|
||||
},
|
||||
"extract_config": null,
|
||||
"faq_config": null,
|
||||
"question_generation_config": {
|
||||
"enabled": false,
|
||||
"question_count": 3
|
||||
},
|
||||
"auto_tag_config": {
|
||||
"enabled": true,
|
||||
"model_id": "8aea788c-bb30-4898-809e-e40c14ffb48c",
|
||||
"max_tags": 3,
|
||||
"skip_if_tagged": true
|
||||
},
|
||||
"is_pinned": false,
|
||||
"pinned_at": null,
|
||||
"knowledge_count": 0,
|
||||
"chunk_count": 0,
|
||||
"processing_count": 0,
|
||||
"vector_store_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"vector_store_name": "elasticsearch-hot",
|
||||
"vector_store_source": "user",
|
||||
"vector_store_engine_type": "elasticsearch",
|
||||
"vector_store_status": "available",
|
||||
"created_at": "2025-08-12T11:30:09.206238645+08:00",
|
||||
"updated_at": "2025-08-12T11:30:09.206238854+08:00",
|
||||
"deleted_at": null
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
### 自动标签配置
|
||||
|
||||
`auto_tag_config` 在文档解析完成后异步调用聊天模型,从知识库已有标签中选择匹配项并增量关联到文档。该过程不会创建新标签,也不会删除或覆盖人工添加的标签。
|
||||
|
||||
| 字段 | 类型 | 默认值 | 说明 |
|
||||
| ---------- | ------- | ------ | ---- |
|
||||
| `enabled` | boolean | `false` | 是否启用自动标签 |
|
||||
| `model_id` | string | `""` | 使用的聊天模型 ID;为空时使用知识库的 `summary_model_id` |
|
||||
| `max_tags` | integer | `3` | 单个文档最多自动关联的标签数,取值范围为 `1` 到 `10` |
|
||||
| `skip_if_tagged` | boolean | `true` | 文档已有标签时是否跳过自动标签。开启时不会调用模型,可避免稀释人工分类;设为 `false` 则在已有标签基础上追加 |
|
||||
|
||||
自动标签仅对启用该配置后新解析或重新解析的文档生效。模型调用失败不会阻塞文档解析完成,异步任务会按照任务队列策略重试。
|
||||
|
||||
候选标签按知识库排序取前 500 个参与分类;标签数超出时会记录告警并使用该前缀,不会跳过任务。模型按候选序号返回结果,服务端会校验序号范围并映射回标签 ID,越界或重复的序号将被丢弃。
|
||||
|
||||
**`vector_store_*` 响应字段说明**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| -------------------------- | ------ | ---------------------------------------------------------------------------------------------------------- |
|
||||
| `vector_store_id` | string | 绑定的向量存储 ID(创建时未指定时为 `null`,从响应中省略) |
|
||||
| `vector_store_name` | string | 绑定存储的展示名。未绑定时返回 `"System default"`;跨空间共享 KB 视图中被隐藏 |
|
||||
| `vector_store_source` | string | `"user"`(DB 中创建的存储)/ `"env"`(环境变量虚拟存储)/ `"shared"`(跨空间共享 KB)/ `"unavailable"`(绑定的存储已不可解析) |
|
||||
| `vector_store_engine_type` | string | 引擎类型(`elasticsearch` / `qdrant` / `milvus` 等)。`shared` / `unavailable` 时为空 |
|
||||
| `vector_store_status` | string | `"available"` / `"unavailable"`。`unavailable` 表示绑定的存储已被删除或不在内存注册表中,UI 可据此提示用户重新绑定 |
|
||||
|
||||
**错误码(Phase 2 新增)**:
|
||||
|
||||
| HTTP | code | 说明 |
|
||||
| ---- | ---- | ----------------------------------------------------------------- |
|
||||
| 400 | 2200 | `vector_store_id` 无效:格式错误、不存在或属于其他空间(统一返回,避免枚举泄漏) |
|
||||
| 400 | 2201 | 指定的向量存储当前不可用:存在于数据库但未注册到引擎注册表,请检查 connection_config |
|
||||
|
||||
## GET `/knowledge-bases` - 获取知识库列表
|
||||
|
||||
返回当前空间拥有的全部知识库。当传入 `agent_id` 时,校验调用者对该共享智能体的访问权限后,返回该智能体配置可见的知识库范围(用于 `@` 提及)。
|
||||
|
||||
**Query 参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| -------- | ------ | ---- | ---------------------------------------------------------- |
|
||||
| agent_id | string | 否 | 共享智能体 ID;传入时按智能体配置(`all` / `selected` / `none`)过滤可见知识库 |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/knowledge-bases' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应**: `data` 为数组,每个元素的字段结构同 `POST /knowledge-bases` 响应,并额外携带 `knowledge_count` / `chunk_count` / `processing_count` / `share_count` / `is_pinned` / `pinned_at` 这些聚合与状态字段。
|
||||
|
||||
> **注意(Phase 2)**:列表接口不包含 `vector_store_name` / `vector_store_source` / `vector_store_engine_type` / `vector_store_status` 这四个解析后的元数据字段(避免 N+1 查询);仅 `vector_store_id` 来自数据库本身。需要展示存储名称时请单独调用详情接口或 `/vector-stores/:id`。
|
||||
|
||||
## GET `/knowledge-bases/:id` - 获取知识库详情
|
||||
|
||||
根据 ID 获取知识库详情。当通过共享智能体访问时,可传 `agent_id` 进行权限校验;此时返回对象会附加 `my_permission` 字段以指示当前用户对该知识库的角色(如 `viewer`)。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ---- | ------ | --------- |
|
||||
| id | string | 知识库 ID |
|
||||
|
||||
**Query 参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| -------- | ------ | ---- | ------------------------------------------ |
|
||||
| agent_id | string | 否 | 共享智能体 ID(用于校验该智能体是否有权访问) |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/knowledge-bases/kb-00000001' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应**: 字段结构同 `POST /knowledge-bases` 响应(包含 Phase 2 的 `vector_store_*` 元数据字段),并附 `is_pinned` / `pinned_at` / `knowledge_count` / `chunk_count` / `processing_count` 状态字段。通过共享智能体访问时还会附加 `my_permission`;同时 `vector_store_name` / `vector_store_engine_type` 会被隐藏(`vector_store_source` 返回 `"shared"`),避免跨空间泄漏存储展示名。
|
||||
|
||||
## PUT `/knowledge-bases/:id` - 更新知识库
|
||||
|
||||
仅知识库 owner(admin)或具备 `editor` 权限的用户可调用。注意:**`vector_store_id` 在创建后不可修改**,更新接口不接收该字段。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ---- | ------ | --------- |
|
||||
| id | string | 知识库 ID |
|
||||
|
||||
**参数说明(请求体)**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ----------- | ------ | ---- | ------------------------------------------------------------- |
|
||||
| name | string | 是 | 知识库名称 |
|
||||
| description | string | 否 | 知识库描述 |
|
||||
| config | object | 否 | 更新配置;包含 `chunking_config` / `image_processing_config` / `faq_config` / `wiki_config` / `indexing_strategy` |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request PUT 'http://localhost:8080/api/v1/knowledge-bases/b5829e4a-3845-4624-a7fb-ea3b35e843b0' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--data '{
|
||||
"name": "weknora new",
|
||||
"description": "weknora description new",
|
||||
"config": {
|
||||
"chunking_config": {
|
||||
"chunk_size": 1000,
|
||||
"chunk_overlap": 200,
|
||||
"separators": [
|
||||
"\n\n",
|
||||
"\n",
|
||||
"。",
|
||||
"!",
|
||||
"?",
|
||||
";",
|
||||
";"
|
||||
],
|
||||
"enable_multimodal": true,
|
||||
"parser_engine_rules": [
|
||||
{
|
||||
"file_types": [".md", ".txt"],
|
||||
"engine": "builtin"
|
||||
}
|
||||
],
|
||||
"enable_parent_child": true,
|
||||
"parent_chunk_size": 4096,
|
||||
"child_chunk_size": 384
|
||||
},
|
||||
"image_processing_config": {
|
||||
"model_id": ""
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**: 字段结构同 `POST /knowledge-bases` 响应(返回更新后的完整知识库对象,包含 Phase 2 的 `vector_store_*` 元数据字段。`vector_store_id` 与创建时保持一致,无法通过该接口更改)。
|
||||
|
||||
## DELETE `/knowledge-bases/:id` - 删除知识库
|
||||
|
||||
仅知识库 owner(与所属空间匹配的 admin)可调用,删除将级联清理知识库下所有知识与切片。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ---- | ------ | --------- |
|
||||
| id | string | 知识库 ID |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request DELETE 'http://localhost:8080/api/v1/knowledge-bases/b5829e4a-3845-4624-a7fb-ea3b35e843b0' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Knowledge base deleted successfully",
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## PUT `/knowledge-bases/:id/pin` - 置顶/取消置顶知识库
|
||||
|
||||
切换知识库的置顶状态。无需请求体,每次调用会自动反转当前 `is_pinned`。置顶时会同步写入 `pinned_at` 时间戳。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ---- | ------ | --------- |
|
||||
| id | string | 知识库 ID |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request PUT 'http://localhost:8080/api/v1/knowledge-bases/kb-00000001/pin' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应**: 字段结构同 `POST /knowledge-bases` 响应(包含 Phase 2 的 `vector_store_*` 元数据字段),本接口操作后 `is_pinned` 翻转、`pinned_at` 同步更新。
|
||||
|
||||
## POST `/knowledge-bases/:id/hybrid-search` - 混合搜索
|
||||
|
||||
在指定知识库内执行向量召回 + 关键词召回的混合检索。请求参数通过 JSON 请求体传递(`SearchParams`)。
|
||||
|
||||
> **兼容说明**:`GET` 方法同样可用(需携带 JSON 请求体),供旧版客户端兼容;新集成请使用 `POST`。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ---- | ------ | --------- |
|
||||
| id | string | 知识库 ID |
|
||||
|
||||
**查询参数**:
|
||||
|
||||
- `resource_urls`: `handle`(默认)或 `public`。`public` 把检索结果 `content` / `image_info` 里的 `resource://` 引用换成可加载的 http(s) 链接,详见[文件与图片引用](./README.md#文件与图片引用resource-与直链)
|
||||
|
||||
**参数说明(请求体)**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ------------------------ | -------- | ---- | ---------------------------------------------------------------- |
|
||||
| query_text | string | 是 | 查询文本 |
|
||||
| vector_threshold | number | 否 | 向量相似度阈值(0-1) |
|
||||
| keyword_threshold | number | 否 | 关键词匹配阈值 |
|
||||
| match_count | integer | 否 | 返回结果数量上限 |
|
||||
| disable_keywords_match | boolean | 否 | 关闭关键词召回 |
|
||||
| disable_vector_match | boolean | 否 | 关闭向量召回 |
|
||||
| knowledge_ids | string[] | 否 | 仅在指定的知识 ID 范围内召回 |
|
||||
| tag_ids | string[] | 否 | 标签过滤(FAQ 类型常用于优先级过滤) |
|
||||
| only_recommended | boolean | 否 | 仅返回标记为推荐的内容 |
|
||||
| knowledge_base_ids | string[] | 否 | 跨知识库召回(需共享相同 embedding 模型),优先级高于路径中的 `:id` |
|
||||
| skip_context_enrichment | boolean | 否 | 跳过父子片段/相邻片段的上下文补全(chat 流程使用) |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request POST 'http://localhost:8080/api/v1/knowledge-bases/kb-00000001/hybrid-search?resource_urls=public' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"query_text": "如何使用知识库",
|
||||
"vector_threshold": 0.5,
|
||||
"match_count": 10
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "chunk-00000001",
|
||||
"content": "知识库是用于存储和检索知识的系统...",
|
||||
"knowledge_id": "knowledge-00000001",
|
||||
"chunk_index": 0,
|
||||
"knowledge_title": "知识库使用指南",
|
||||
"start_at": 0,
|
||||
"end_at": 500,
|
||||
"seq": 1,
|
||||
"score": 0.95,
|
||||
"chunk_type": "text",
|
||||
"image_info": "",
|
||||
"metadata": {},
|
||||
"knowledge_filename": "guide.pdf",
|
||||
"knowledge_source": "file"
|
||||
}
|
||||
],
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/knowledge-bases/copy` - 拷贝知识库
|
||||
|
||||
异步拷贝整个知识库(配置 + 全部知识内容)。请求会被入队到 Asynq 后台任务(队列 `default`,最多重试 3 次),并立即返回 `task_id` 供轮询进度。
|
||||
|
||||
**约束**:源知识库 `source_id` 必须属于调用者所在空间;若指定 `target_id`,目标知识库同样必须属于调用者空间,否则返回 `403 Forbidden`。
|
||||
|
||||
**Phase 2 同步预检(当 `target_id` 非空时)**:
|
||||
|
||||
| 检查 | 失败时响应 |
|
||||
| -------------- | ----------------------------------------------------------------------------- |
|
||||
| 嵌入模型一致性 | `400` `source and target knowledge bases use different embedding models; clone into a target with the same embedding model` |
|
||||
| 向量存储一致性 | `400` `source and target knowledge bases are bound to different vector stores; cross-store cloning is not yet supported` |
|
||||
|
||||
预检失败时任务不会入队、不会生成 `task_id`,调用方直接收到 `400`;这两个检查在异步 worker 中会再次执行(defense in depth),但在握手时即时拒绝是为了避免用户需要轮询 `progress` 才能看到错误。当 `target_id` 为空(新建目标库)时,目标库会自动复制源库的 `vector_store_id` 与 `embedding_model_id`,因此预检不会触发。
|
||||
|
||||
**参数说明(请求体)**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ---------- | ------ | ---- | ------------------------------------------------------------- |
|
||||
| source_id | string | 是 | 源知识库 ID(必须属于当前空间) |
|
||||
| target_id | string | 否 | 目标知识库 ID(若复用已存在知识库;同样必须属于当前空间) |
|
||||
| task_id | string | 否 | 自定义任务 ID;不传则由服务端生成(基于空间、源 ID、时间戳) |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/knowledge-bases/copy' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"source_id": "kb-00000001"
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"task_id": "kb_clone_1_kb-00000001_1736582400",
|
||||
"source_id": "kb-00000001",
|
||||
"target_id": "",
|
||||
"message": "Knowledge base copy task started"
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## GET `/knowledge-bases/copy/progress/:task_id` - 获取拷贝进度
|
||||
|
||||
查询拷贝任务的当前状态与进度(数据由 worker 写入 Redis)。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ------- | ------ | ------------------------------------------------- |
|
||||
| task_id | string | 由 `POST /knowledge-bases/copy` 返回的任务 ID |
|
||||
|
||||
**响应字段(`data`)**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ---------- | ------- | ------------------------------------------------------------ |
|
||||
| task_id | string | 任务 ID |
|
||||
| source_id | string | 源知识库 ID |
|
||||
| target_id | string | 目标知识库 ID(任务开始后填入) |
|
||||
| status | string | `pending` / `processing` / `completed` / `failed` |
|
||||
| progress | integer | 进度百分比 0–100 |
|
||||
| total | integer | 计划拷贝的知识总数 |
|
||||
| processed | integer | 已处理的知识数 |
|
||||
| message | string | 当前状态描述 |
|
||||
| error | string | 失败时的错误信息 |
|
||||
| created_at | integer | 任务创建时间(Unix 秒) |
|
||||
| updated_at | integer | 最后更新时间(Unix 秒) |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/knowledge-bases/copy/progress/kb_clone_1_kb-00000001_1736582400' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"task_id": "kb_clone_1_kb-00000001_1736582400",
|
||||
"source_id": "kb-00000001",
|
||||
"target_id": "kb-00000002",
|
||||
"status": "completed",
|
||||
"progress": 100,
|
||||
"total": 10,
|
||||
"processed": 10,
|
||||
"message": "Task completed successfully",
|
||||
"error": "",
|
||||
"created_at": 1736582400,
|
||||
"updated_at": 1736582460
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/knowledge-bases/:id/duplicate` - 创建知识库副本
|
||||
|
||||
同步创建一个**仅包含设置**的新知识库副本。会复制分块、模型、索引策略、Wiki/FAQ 配置等设置字段,但**不会**复制知识条目、分块内容、FAQ 条目、Wiki 页面、向量/关键词索引、数据源绑定、分享关系或置顶状态。
|
||||
|
||||
与 `POST /knowledge-bases/copy` 的区别:
|
||||
|
||||
| 能力 | `/duplicate` | `/copy` |
|
||||
| ---- | ------------ | ------- |
|
||||
| 执行方式 | 同步,立即返回新 KB | 异步任务,需轮询 progress |
|
||||
| 复制内容 | 仅设置 | 设置 + 全部知识内容 |
|
||||
| 新 KB ID | 服务端自动生成 UUID | 可指定已有目标库或新建 |
|
||||
|
||||
**权限**:需要 `Contributor+`,且对源知识库至少有 `Viewer` 读权限(路由层 `KBAccessRead`)。源知识库必须属于调用者所在空间,否则返回 `403 Forbidden`。
|
||||
|
||||
**命名规则**:新 KB 名称在源名称后追加本地化后缀(依据 `Accept-Language` 或 `WEKNORA_LANGUAGE`),例如中文 `原名 副本`、英文 `Original Name Copy`;若同名已存在则递增为 `原名 副本 2`、`Original Name Copy 2` 等。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ---- | ------ | ----------- |
|
||||
| id | string | 源知识库 ID |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/knowledge-bases/kb-00000001/duplicate' \
|
||||
--header 'Authorization: Bearer <token>' \
|
||||
--header 'Accept-Language: zh-CN' \
|
||||
--request POST
|
||||
```
|
||||
|
||||
**响应**(HTTP 201):
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"source_id": "kb-00000001",
|
||||
"target_id": "kb-00000002",
|
||||
"message": "Knowledge base duplicate created",
|
||||
"knowledge_base": {
|
||||
"id": "kb-00000002",
|
||||
"name": "产品文档 副本",
|
||||
"type": "document",
|
||||
"description": "…",
|
||||
"embedding_model_id": "embed-1",
|
||||
"chunking_config": {},
|
||||
"knowledge_count": 0,
|
||||
"chunk_count": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**响应字段(`data`)**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --------------- | ------ | ---------------------------- |
|
||||
| source_id | string | 源知识库 ID |
|
||||
| target_id | string | 新创建的知识库 ID |
|
||||
| message | string | 操作结果描述 |
|
||||
| knowledge_base | object | 新副本的完整知识库对象 |
|
||||
|
||||
**常见错误**:
|
||||
|
||||
| 场景 | HTTP | 说明 |
|
||||
| -------------------- | ---- | ---- |
|
||||
| 源知识库不存在 | 404 | `Source knowledge base not found` |
|
||||
| 源库属于其他空间 | 403 | `No permission to duplicate this knowledge base` |
|
||||
| 向量存储绑定无效 | 400 | 源库绑定的 vector store 不可用 |
|
||||
|
||||
## GET `/knowledge-bases/:id/move-targets` - 获取可迁移目标知识库列表
|
||||
|
||||
返回当前知识库的内容**可以迁移到**的目标知识库列表。筛选规则:
|
||||
|
||||
- 与源知识库 `type` 相同
|
||||
- 与源知识库 `embedding_model_id` 相同
|
||||
- 非临时知识库(`is_temporary = false`)
|
||||
- 不包含源知识库自身
|
||||
- 仅同空间的知识库
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ---- | ------ | ------------- |
|
||||
| id | string | 源知识库 ID |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/knowledge-bases/kb-00000001/move-targets' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "kb-00000002",
|
||||
"name": "技术文档知识库",
|
||||
"description": "技术文档相关知识",
|
||||
"type": "document",
|
||||
"is_temporary": false,
|
||||
"tenant_id": 1,
|
||||
"chunking_config": {
|
||||
"chunk_size": 1000,
|
||||
"chunk_overlap": 200,
|
||||
"separators": ["\n\n", "\n"],
|
||||
"enable_multimodal": true,
|
||||
"parser_engine_rules": [],
|
||||
"enable_parent_child": false,
|
||||
"parent_chunk_size": 4096,
|
||||
"child_chunk_size": 384
|
||||
},
|
||||
"image_processing_config": {
|
||||
"model_id": ""
|
||||
},
|
||||
"embedding_model_id": "dff7bc94-7885-4dd1-bfd5-bd96e4df2fc3",
|
||||
"summary_model_id": "8aea788c-bb30-4898-809e-e40c14ffb48c",
|
||||
"vlm_config": {
|
||||
"enabled": false,
|
||||
"model_id": ""
|
||||
},
|
||||
"asr_config": {
|
||||
"enabled": false,
|
||||
"model_id": "",
|
||||
"language": ""
|
||||
},
|
||||
"storage_provider_config": {
|
||||
"provider": "local"
|
||||
},
|
||||
"storage_config": {
|
||||
"secret_id": "",
|
||||
"secret_key": "",
|
||||
"region": "",
|
||||
"bucket_name": "",
|
||||
"app_id": "",
|
||||
"path_prefix": ""
|
||||
},
|
||||
"extract_config": null,
|
||||
"faq_config": null,
|
||||
"question_generation_config": null,
|
||||
"is_pinned": false,
|
||||
"pinned_at": null,
|
||||
"knowledge_count": 8,
|
||||
"chunk_count": 210,
|
||||
"processing_count": 0,
|
||||
"created_at": "2025-08-12T11:30:09.206238+08:00",
|
||||
"updated_at": "2025-08-12T11:30:09.206238+08:00",
|
||||
"deleted_at": null
|
||||
}
|
||||
],
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,101 @@
|
||||
# 知识搜索 API
|
||||
|
||||
[返回目录](./README.md)
|
||||
|
||||
| 方法 | 路径 | 描述 |
|
||||
| ---- | ------------------ | -------- |
|
||||
| POST | `/knowledge-search` | 知识搜索 |
|
||||
|
||||
## POST `/knowledge-search` - 知识搜索
|
||||
|
||||
在知识库中搜索相关内容(不使用 LLM 总结),直接返回检索结果。
|
||||
|
||||
**参数说明(请求体)**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ------------------- | -------- | ---- | ---------------------------------------------------------- |
|
||||
| query | string | 是 | 搜索查询文本 |
|
||||
| knowledge_base_id | string | 否 | 单个知识库 ID(向后兼容);与 `knowledge_base_ids` 互斥 |
|
||||
| knowledge_base_ids | string[] | 否 | 多个知识库 ID 列表,跨知识库搜索 |
|
||||
| knowledge_ids | string[] | 否 | 进一步限定到指定知识(文件);不传则在整库范围内搜索 |
|
||||
|
||||
> 必须指定 `knowledge_base_id` 或 `knowledge_base_ids` 中的至少一个。
|
||||
|
||||
**查询参数**:
|
||||
|
||||
- `resource_urls`: `handle`(默认)或 `public`。`public` 把检索结果 `content` / `image_info` 里的 `resource://` 引用换成可加载的 http(s) 链接,详见[文件与图片引用](./README.md#文件与图片引用resource-与直链)
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
# 搜索单个知识库
|
||||
curl --location 'http://localhost:8080/api/v1/knowledge-search' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"query": "如何使用知识库",
|
||||
"knowledge_base_id": "kb-00000001"
|
||||
}'
|
||||
|
||||
# 搜索多个知识库
|
||||
curl --location 'http://localhost:8080/api/v1/knowledge-search' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"query": "如何使用知识库",
|
||||
"knowledge_base_ids": ["kb-00000001", "kb-00000002"]
|
||||
}'
|
||||
|
||||
# 搜索指定文件
|
||||
curl --location 'http://localhost:8080/api/v1/knowledge-search' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"query": "如何使用知识库",
|
||||
"knowledge_ids": ["4c4e7c1a-09cf-485b-a7b5-24b8cdc5acf5"]
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "chunk-00000001",
|
||||
"content": "知识库是用于存储和检索知识的系统...",
|
||||
"knowledge_id": "knowledge-00000001",
|
||||
"chunk_index": 0,
|
||||
"knowledge_title": "知识库使用指南",
|
||||
"start_at": 0,
|
||||
"end_at": 500,
|
||||
"seq": 1,
|
||||
"score": 0.95,
|
||||
"chunk_type": "text",
|
||||
"image_info": "",
|
||||
"metadata": {},
|
||||
"knowledge_filename": "guide.pdf",
|
||||
"knowledge_source": "file"
|
||||
}
|
||||
],
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
**响应字段说明(data[])**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ------------------ | ------- | ------------------------------------- |
|
||||
| id | string | 分块 ID |
|
||||
| content | string | 命中的分块文本 |
|
||||
| knowledge_id | string | 该分块所属的知识 ID |
|
||||
| chunk_index | int | 分块在知识中的序号 |
|
||||
| knowledge_title | string | 来源知识标题 |
|
||||
| start_at / end_at | int | 分块在源文档中的字符偏移 |
|
||||
| seq | int | 命中排序号 |
|
||||
| score | number | 相似度(rerank 后归一化后的最终得分) |
|
||||
| chunk_type | string | 分块类型(`text` / `image` / ...) |
|
||||
| image_info | string | 图像分块的额外信息(JSON 字符串) |
|
||||
| metadata | object | 自定义元数据 |
|
||||
| knowledge_filename | string | 来源文件名 |
|
||||
| knowledge_source | string | 来源类型(`file` / `url` / `manual`) |
|
||||
@@ -0,0 +1,557 @@
|
||||
# MCP Service API
|
||||
|
||||
[返回目录](./README.md)
|
||||
|
||||
MCP(Model Context Protocol)服务管理接口,提供 MCP 服务的 CRUD、连通性测试、工具/资源发现,以及工具人工审批策略配置。
|
||||
|
||||
| 方法 | 路径 | 描述 |
|
||||
| ------ | ------------------------------------------------- | --------------------------------------------- |
|
||||
| POST | `/mcp-services` | 创建 MCP 服务 |
|
||||
| GET | `/mcp-services` | 获取当前空间的 MCP 服务列表 |
|
||||
| GET | `/mcp-services/:id` | 获取 MCP 服务详情 |
|
||||
| PUT | `/mcp-services/:id` | 更新 MCP 服务(部分字段更新) |
|
||||
| DELETE | `/mcp-services/:id` | 删除 MCP 服务 |
|
||||
| POST | `/mcp-services/:id/test` | 测试 MCP 服务连通性 |
|
||||
| GET | `/mcp-services/:id/tools` | 获取 MCP 服务工具列表 |
|
||||
| GET | `/mcp-services/:id/resources` | 获取 MCP 服务资源列表 |
|
||||
| GET | `/mcp-services/:id/tool-approvals` | 列出该服务下各工具的人工审批策略 |
|
||||
| PUT | `/mcp-services/:id/tool-approvals/:tool_name` | 设置/更新某工具的人工审批策略 |
|
||||
| POST | `/agent/tool-approvals/:pending_id` | 处理 Agent 工具调用待审批请求 |
|
||||
|
||||
## POST `/mcp-services` - 创建 MCP 服务
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ---------------- | ------- | ---- | --------------------------------------------------------------------------------------------- |
|
||||
| name | string | 是 | 服务名称 |
|
||||
| description | string | 否 | 服务描述 |
|
||||
| transport_type | string | 是 | 传输类型,可选:`sse`、`http-streamable`、`stdio` |
|
||||
| url | string | 条件 | 服务地址;当 `transport_type` 为 `sse` / `http-streamable` 时必填(受 SSRF 安全校验约束) |
|
||||
| headers | object | 否 | 自定义请求头 |
|
||||
| auth_config | object | 否 | 认证配置,支持 `api_key`、`token` |
|
||||
| advanced_config | object | 否 | 高级配置,支持 `timeout`、`retry_count`、`retry_delay` |
|
||||
| stdio_config | object | 条件 | stdio 传输配置,包含 `command`、`args`;当 `transport_type` 为 `stdio` 时必填 |
|
||||
| env_vars | object | 否 | 环境变量(stdio 场景常用) |
|
||||
| enabled | boolean | 否 | 是否启用 |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/mcp-services' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name": "天气查询服务",
|
||||
"description": "提供全球天气信息查询",
|
||||
"transport_type": "sse",
|
||||
"url": "https://mcp.example.com/weather/sse",
|
||||
"headers": {
|
||||
"X-Custom-Header": "value"
|
||||
},
|
||||
"auth_config": {
|
||||
"api_key": "weather-api-key-xxxxx"
|
||||
},
|
||||
"advanced_config": {
|
||||
"timeout": 30,
|
||||
"retry_count": 3,
|
||||
"retry_delay": 1
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "mcp-00000001",
|
||||
"tenant_id": 1,
|
||||
"name": "天气查询服务",
|
||||
"description": "提供全球天气信息查询",
|
||||
"enabled": true,
|
||||
"transport_type": "sse",
|
||||
"url": "https://mcp.example.com/weather/sse",
|
||||
"headers": {
|
||||
"X-Custom-Header": "value"
|
||||
},
|
||||
"auth_config": {
|
||||
"api_key": "weather-api-key-xxxxx"
|
||||
},
|
||||
"advanced_config": {
|
||||
"timeout": 30,
|
||||
"retry_count": 3,
|
||||
"retry_delay": 1
|
||||
},
|
||||
"is_builtin": false,
|
||||
"created_at": "2025-08-12T10:00:00+08:00",
|
||||
"updated_at": "2025-08-12T10:00:00+08:00"
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
**创建 stdio 类型的 MCP 服务示例**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/mcp-services' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name": "本地文件服务",
|
||||
"description": "通过 stdio 访问本地文件系统",
|
||||
"transport_type": "stdio",
|
||||
"stdio_config": {
|
||||
"command": "/usr/local/bin/mcp-file-server",
|
||||
"args": ["--root", "/data"]
|
||||
},
|
||||
"env_vars": {
|
||||
"MCP_LOG_LEVEL": "info"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## GET `/mcp-services` - 获取 MCP 服务列表
|
||||
|
||||
返回当前空间已配置的所有 MCP 服务。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/mcp-services' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "mcp-00000001",
|
||||
"tenant_id": 1,
|
||||
"name": "天气查询服务",
|
||||
"description": "提供全球天气信息查询",
|
||||
"enabled": true,
|
||||
"transport_type": "sse",
|
||||
"url": "https://mcp.example.com/weather/sse",
|
||||
"headers": {},
|
||||
"auth_config": {
|
||||
"api_key": "weather-api-key-xxxxx"
|
||||
},
|
||||
"advanced_config": {
|
||||
"timeout": 30,
|
||||
"retry_count": 3,
|
||||
"retry_delay": 1
|
||||
},
|
||||
"is_builtin": false,
|
||||
"created_at": "2025-08-12T10:00:00+08:00",
|
||||
"updated_at": "2025-08-12T10:00:00+08:00"
|
||||
},
|
||||
{
|
||||
"id": "mcp-00000002",
|
||||
"tenant_id": 1,
|
||||
"name": "本地文件服务",
|
||||
"description": "通过 stdio 访问本地文件系统",
|
||||
"enabled": true,
|
||||
"transport_type": "stdio",
|
||||
"headers": {},
|
||||
"auth_config": null,
|
||||
"advanced_config": null,
|
||||
"stdio_config": {
|
||||
"command": "/usr/local/bin/mcp-file-server",
|
||||
"args": ["--root", "/data"]
|
||||
},
|
||||
"env_vars": {
|
||||
"MCP_LOG_LEVEL": "info"
|
||||
},
|
||||
"is_builtin": false,
|
||||
"created_at": "2025-08-12T11:00:00+08:00",
|
||||
"updated_at": "2025-08-12T11:00:00+08:00"
|
||||
}
|
||||
],
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## GET `/mcp-services/:id` - 获取 MCP 服务详情
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ---- | ------ | -------------- |
|
||||
| id | string | MCP 服务 ID |
|
||||
|
||||
> 注:内置(`is_builtin: true`)服务在响应中会隐藏敏感凭证字段。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/mcp-services/mcp-00000001' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "mcp-00000001",
|
||||
"tenant_id": 1,
|
||||
"name": "天气查询服务",
|
||||
"description": "提供全球天气信息查询",
|
||||
"enabled": true,
|
||||
"transport_type": "sse",
|
||||
"url": "https://mcp.example.com/weather/sse",
|
||||
"headers": {},
|
||||
"auth_config": {
|
||||
"api_key": "weather-api-key-xxxxx"
|
||||
},
|
||||
"advanced_config": {
|
||||
"timeout": 30,
|
||||
"retry_count": 3,
|
||||
"retry_delay": 1
|
||||
},
|
||||
"is_builtin": false,
|
||||
"created_at": "2025-08-12T10:00:00+08:00",
|
||||
"updated_at": "2025-08-12T10:00:00+08:00"
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## PUT `/mcp-services/:id` - 更新 MCP 服务
|
||||
|
||||
支持部分字段更新,可传入下列任意子集:`name`、`description`、`enabled`、`transport_type`、`url`、`stdio_config`、`env_vars`、`headers`、`auth_config`、`advanced_config`。其中 `url` 若提供,会再次执行 SSRF 安全校验。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request PUT 'http://localhost:8080/api/v1/mcp-services/mcp-00000001' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name": "天气查询服务(更新)",
|
||||
"description": "提供全球天气信息查询,支持实时数据",
|
||||
"enabled": false
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "mcp-00000001",
|
||||
"tenant_id": 1,
|
||||
"name": "天气查询服务(更新)",
|
||||
"description": "提供全球天气信息查询,支持实时数据",
|
||||
"enabled": false,
|
||||
"transport_type": "sse",
|
||||
"url": "https://mcp.example.com/weather/sse",
|
||||
"headers": {},
|
||||
"auth_config": {
|
||||
"api_key": "weather-api-key-xxxxx"
|
||||
},
|
||||
"advanced_config": {
|
||||
"timeout": 30,
|
||||
"retry_count": 3,
|
||||
"retry_delay": 1
|
||||
},
|
||||
"is_builtin": false,
|
||||
"created_at": "2025-08-12T10:00:00+08:00",
|
||||
"updated_at": "2025-08-12T12:00:00+08:00"
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## DELETE `/mcp-services/:id` - 删除 MCP 服务
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request DELETE 'http://localhost:8080/api/v1/mcp-services/mcp-00000001' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "MCP service deleted successfully"
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/mcp-services/:id/test` - 测试 MCP 服务连通性
|
||||
|
||||
后端会以已保存配置建立一次 MCP 连接,返回连接结果及探测到的工具/资源列表。连接失败时 HTTP 仍为 200,但 `data.success` 为 `false`,错误原因在 `data.message` 中。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request POST 'http://localhost:8080/api/v1/mcp-services/mcp-00000001/test' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"success": true,
|
||||
"message": "连接成功",
|
||||
"description": "提供全球天气信息查询",
|
||||
"tools": [
|
||||
{
|
||||
"name": "get_weather",
|
||||
"description": "获取指定城市的天气信息",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"city": {
|
||||
"type": "string",
|
||||
"description": "城市名称"
|
||||
}
|
||||
},
|
||||
"required": ["city"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"resources": [
|
||||
{
|
||||
"uri": "weather://cities",
|
||||
"name": "城市列表",
|
||||
"description": "支持查询的城市列表",
|
||||
"mimeType": "application/json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## GET `/mcp-services/:id/tools` - 获取 MCP 服务工具列表
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/mcp-services/mcp-00000001/tools' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"name": "get_weather",
|
||||
"description": "获取指定城市的天气信息",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"city": {
|
||||
"type": "string",
|
||||
"description": "城市名称"
|
||||
}
|
||||
},
|
||||
"required": ["city"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "get_forecast",
|
||||
"description": "获取未来天气预报",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"city": {
|
||||
"type": "string",
|
||||
"description": "城市名称"
|
||||
},
|
||||
"days": {
|
||||
"type": "integer",
|
||||
"description": "预报天数"
|
||||
}
|
||||
},
|
||||
"required": ["city"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## GET `/mcp-services/:id/resources` - 获取 MCP 服务资源列表
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/mcp-services/mcp-00000001/resources' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"uri": "weather://cities",
|
||||
"name": "城市列表",
|
||||
"description": "支持查询的城市列表",
|
||||
"mimeType": "application/json"
|
||||
},
|
||||
{
|
||||
"uri": "weather://config",
|
||||
"name": "服务配置",
|
||||
"description": "当前服务配置信息",
|
||||
"mimeType": "application/json"
|
||||
}
|
||||
],
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## GET `/mcp-services/:id/tool-approvals` - 列出工具人工审批策略
|
||||
|
||||
返回该 MCP 服务下各工具持久化的 `require_approval` 标记。仅返回数据库中已显式配置过的工具记录;未出现在列表中的工具默认无需审批。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ---- | ------ | ----------- |
|
||||
| id | string | MCP 服务 ID |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/mcp-services/mcp-00000001/tool-approvals' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"tool_name": "delete_file",
|
||||
"require_approval": true,
|
||||
"updated_at": "2025-09-20T15:30:00+08:00"
|
||||
},
|
||||
{
|
||||
"tool_name": "get_weather",
|
||||
"require_approval": false,
|
||||
"updated_at": "2025-09-20T15:31:00+08:00"
|
||||
}
|
||||
],
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## PUT `/mcp-services/:id/tool-approvals/:tool_name` - 设置工具人工审批策略
|
||||
|
||||
为指定 MCP 服务下的某个工具设置/更新人工审批要求。当 `require_approval` 为 `true` 时,Agent 在调用该工具前会阻塞并产生一条待审批记录,需要前端调用 `POST /agent/tool-approvals/:pending_id` 完成审批。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ---------- | ------ | ------------------------------------------------------------------- |
|
||||
| id | string | MCP 服务 ID |
|
||||
| tool_name | string | 工具名(由 Gin 自动 URL 解码,调用方需对名称中的 `%`、`/` 做 URL 编码) |
|
||||
|
||||
**请求体**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------- | ---- | ----------------------------------- |
|
||||
| require_approval | boolean | 是 | 是否要求人工审批后才能执行该工具 |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request PUT 'http://localhost:8080/api/v1/mcp-services/mcp-00000001/tool-approvals/delete_file' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"require_approval": true
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/agent/tool-approvals/:pending_id` - 处理待审批工具调用
|
||||
|
||||
用于 Agent 在执行过程中阻塞等待人工审批的场景:当 Agent 命中一个 `require_approval = true` 的工具时会生成一条 `pending_id`,前端拿到这个 ID 后调用此接口将审批结果回传给 Agent,Agent 才会继续执行(或终止)。
|
||||
|
||||
**鉴权要求**:请求上下文中必须有已认证用户(`user_id`),且该用户必须是当前 pending 会话的所有者;空间与用户两层都会做 fail-close 校验。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ----------- | ------ | ------------------- |
|
||||
| pending_id | string | 待审批记录 ID |
|
||||
|
||||
**请求体**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| -------------- | ------ | ---- | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| decision | string | 是 | 审批结论,必须为 `approve` 或 `reject` |
|
||||
| modified_args | object | 否 | 仅在 `approve` 时生效,允许人工修改本次工具调用的参数;必须是非 null 的 JSON 对象,否则返回 400 |
|
||||
| reason | string | 否 | 审批理由(任意,便于审计) |
|
||||
|
||||
**请求(通过)**:
|
||||
|
||||
```curl
|
||||
curl --location --request POST 'http://localhost:8080/api/v1/agent/tool-approvals/pending-abcdef123456' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"decision": "approve",
|
||||
"modified_args": {
|
||||
"path": "/tmp/safe-target.txt"
|
||||
},
|
||||
"reason": "已确认目标路径安全"
|
||||
}'
|
||||
```
|
||||
|
||||
**请求(驳回)**:
|
||||
|
||||
```curl
|
||||
curl --location --request POST 'http://localhost:8080/api/v1/agent/tool-approvals/pending-abcdef123456' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"decision": "reject",
|
||||
"reason": "目标路径在受保护目录"
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
**错误码说明**:
|
||||
|
||||
| HTTP | 触发条件 |
|
||||
| ---- | --------------------------------------------------------------------------------------- |
|
||||
| 400 | `decision` 不是 `approve`/`reject`;或 `modified_args` 是 `null`/非对象;或空间/用户错配 |
|
||||
| 401 | 上下文缺失认证用户(中间件未注入 `user_id`) |
|
||||
| 404 | `pending_id` 不存在或已完成(超时/取消已先一步消费) |
|
||||
@@ -0,0 +1,96 @@
|
||||
# 长期记忆 API
|
||||
|
||||
[返回目录](./README.md)
|
||||
|
||||
长期记忆空间始终绑定在**当前调用者**上:路径里没有 subject id,服务端从凭证推导身份。因此 Viewer+ 的会话令牌,或 **full-access API Key** 才能访问;带知识库范围的集成 Key 不能继承某个人的记忆。
|
||||
|
||||
工作空间管理员先在设置里打开空间级开关,用户还可以再关掉自己的记忆。推断出的条目会停在 `pending`,确认后才进入提示词。
|
||||
|
||||
| 方法 | 路径 | 描述 |
|
||||
| ---- | ---- | ---- |
|
||||
| GET | `/memory/settings` | 获取合并后的记忆开关(空间级 + 个人级)与条数 |
|
||||
| PUT | `/memory/settings` | 开启或关闭当前用户自己的长期记忆 |
|
||||
| GET | `/memory/items` | 分页列出记忆,可按状态过滤 |
|
||||
| POST | `/memory/items` | 手动新增一条记忆 |
|
||||
| PUT | `/memory/items/{id}` | 修改内容与重要度(之后不会被后台抽取覆盖) |
|
||||
| DELETE | `/memory/items/{id}` | 永久删除一条记忆 |
|
||||
| POST | `/memory/items/{id}/confirm` | 确认一条推断出的记忆 |
|
||||
| POST | `/memory/items/{id}/reject` | 否决一条推断出的记忆 |
|
||||
| DELETE | `/memory/items` | 清空当前用户的全部记忆 |
|
||||
| GET | `/memory/topics` | 列出尚未提升为长期关注的主题计数 |
|
||||
| POST | `/memory/topics/{id}/promote` | 立即把主题记为长期关注 |
|
||||
| DELETE | `/memory/topics/{id}` | 停止跟踪一个主题 |
|
||||
| GET | `/memory/documents` | 列出反复引用的文档(未达习惯门槛的不展示) |
|
||||
| DELETE | `/memory/documents/{id}` | 停止用某份文档做个性化检索 |
|
||||
| GET | `/memory/export` | 以 JSON 导出全部记忆 |
|
||||
| POST | `/memory/consolidate` | 立刻整理(合并近义条目、归档到期事项) |
|
||||
|
||||
## GET `/memory/settings`
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/memory/settings' \
|
||||
--header 'Authorization: Bearer <token>'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"workspace_enabled": true,
|
||||
"user_enabled": true,
|
||||
"effective": true,
|
||||
"write_mode": "auto",
|
||||
"item_count": 12,
|
||||
"max_items": 200
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`write_mode` 为 `explicit_only`(只记用户明确要求记住的)或 `auto`(后台从对话蒸馏)。`effective` = 空间开关 ∧ 个人开关。
|
||||
|
||||
## PUT `/memory/settings`
|
||||
|
||||
```curl
|
||||
curl --location --request PUT 'http://localhost:8080/api/v1/memory/settings' \
|
||||
--header 'Authorization: Bearer <token>' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{"enabled": true}'
|
||||
```
|
||||
|
||||
`enabled` 必填。只改个人开关,不能用这个接口改空间级配置。
|
||||
|
||||
## GET `/memory/items`
|
||||
|
||||
查询参数:
|
||||
|
||||
| 参数 | 说明 |
|
||||
| ---- | ---- |
|
||||
| `status` | 可选:`active` / `superseded` / `archived` / `pending`;省略则不过滤 |
|
||||
| `limit` | 默认 50,最大 200 |
|
||||
| `offset` | 默认 0 |
|
||||
|
||||
`kind` 取值:`profile` / `preference` / `fact` / `task` / `interest`。
|
||||
|
||||
## POST `/memory/items`
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "preference",
|
||||
"content": "回答直接给结论,少铺垫",
|
||||
"importance": 3
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/memory/items/{id}/confirm` / `reject`
|
||||
|
||||
推断出的记忆(`status=pending`)确认后才注入提示词;否决会留下 tombstone,避免下一轮蒸馏把同一句话再写回来。
|
||||
|
||||
## GET `/memory/export`
|
||||
|
||||
返回 `{success, total, truncated, data}`,并带 `Content-Disposition: attachment; filename="weknora-memories.json"`。`truncated` 仅在触达导出上限(2 万条)时为 true。
|
||||
|
||||
## POST `/memory/consolidate`
|
||||
|
||||
不等待每日后台整理,立刻合并意思接近的条目并归档到期事项。返回 `merged` / `demoted` / `expired` / `reviewed` / `candidates`;若什么都没合并,`skipped` 会说明原因(例如 `too_few_items`)。
|
||||
@@ -0,0 +1,267 @@
|
||||
# 消息管理 API
|
||||
|
||||
[返回目录](./README.md)
|
||||
|
||||
| 方法 | 路径 | 描述 |
|
||||
| ------ | ---------------------------- | ------------------------ |
|
||||
| GET | `/messages/:session_id/load` | 获取最近的会话消息列表 |
|
||||
| DELETE | `/messages/:session_id/:id` | 删除消息 |
|
||||
| POST | `/messages/search` | 搜索历史对话 |
|
||||
| GET | `/messages/chat-history-stats` | 获取聊天历史知识库统计 |
|
||||
|
||||
## GET `/messages/:session_id/load` - 获取最近的会话消息列表
|
||||
|
||||
**查询参数**:
|
||||
|
||||
- `before_time`: 上一次拉取的最早一条消息的 created_at 字段,为空拉取最近的消息
|
||||
- `limit`: 每页条数(默认 20)
|
||||
- `resource_urls`: `handle`(默认)或 `public`。`public` 把历史消息里的 `resource://` 图片引用换成可加载的 http(s) 链接,详见[文件与图片引用](./README.md#文件与图片引用resource-与直链)
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request GET 'http://localhost:8080/api/v1/messages/ceb9babb-1e30-41d7-817d-fd584954304b/load?limit=3&before_time=2030-08-12T14%3A35%3A42.123456789Z' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"query": "彗尾的形状"
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "b8b90eeb-7dd5-4cf9-81c6-5ebcbd759451",
|
||||
"session_id": "ceb9babb-1e30-41d7-817d-fd584954304b",
|
||||
"request_id": "hCA8SDjxcAvv",
|
||||
"content": "<think>\n好的",
|
||||
"role": "assistant",
|
||||
"knowledge_references": [
|
||||
{
|
||||
"id": "c8347bef-127f-4a22-b962-edf5a75386ec",
|
||||
"content": "彗星xxx",
|
||||
"knowledge_id": "a6790b93-4700-4676-bd48-0d4804e1456b",
|
||||
"chunk_index": 0,
|
||||
"knowledge_title": "彗星.txt",
|
||||
"start_at": 0,
|
||||
"end_at": 2760,
|
||||
"seq": 0,
|
||||
"score": 4.038836479187012,
|
||||
"match_type": 4,
|
||||
"sub_chunk_id": [
|
||||
"688821f0-40bf-428e-8cb6-541531ebeb76",
|
||||
"c1e9903e-2b4d-4281-be15-0149288d45c2",
|
||||
"7d955251-3f79-4fd5-a6aa-02f81e044091"
|
||||
],
|
||||
"metadata": {},
|
||||
"chunk_type": "text",
|
||||
"parent_chunk_id": "",
|
||||
"image_info": "",
|
||||
"knowledge_filename": "彗星.txt",
|
||||
"knowledge_source": ""
|
||||
},
|
||||
{
|
||||
"id": "fa3aadee-cadb-4a84-9941-c839edc3e626",
|
||||
"content": "# 文档名称\n彗星.txt\n\n# 摘要\n彗星是由冰和尘埃构成的太阳系小天体,接近太阳时会释放气体形成彗发和彗尾。其轨道周期差异大,来源包括柯伊伯带和奥尔特云。彗星与小行星的区别逐渐模糊,部分彗星已失去挥发物质,类似小行星。目前已知彗星数量众多,且存在系外彗星。彗星在古代被视为凶兆,现代研究揭示其复杂结构与起源。",
|
||||
"knowledge_id": "a6790b93-4700-4676-bd48-0d4804e1456b",
|
||||
"chunk_index": 6,
|
||||
"knowledge_title": "彗星.txt",
|
||||
"start_at": 0,
|
||||
"end_at": 0,
|
||||
"seq": 6,
|
||||
"score": 0.6131043121858466,
|
||||
"match_type": 0,
|
||||
"sub_chunk_id": null,
|
||||
"metadata": {},
|
||||
"chunk_type": "summary",
|
||||
"parent_chunk_id": "c8347bef-127f-4a22-b962-edf5a75386ec",
|
||||
"image_info": "",
|
||||
"knowledge_filename": "彗星.txt",
|
||||
"knowledge_source": ""
|
||||
}
|
||||
],
|
||||
"agent_steps": [],
|
||||
"is_completed": true,
|
||||
"is_fallback": false,
|
||||
"agent_duration_ms": 2500,
|
||||
"channel": "web",
|
||||
"created_at": "2025-08-12T10:24:38.370548+08:00",
|
||||
"updated_at": "2025-08-12T10:25:40.416382+08:00",
|
||||
"deleted_at": null
|
||||
},
|
||||
{
|
||||
"id": "7fa136ae-a045-424e-baac-52113d92ae94",
|
||||
"session_id": "ceb9babb-1e30-41d7-817d-fd584954304b",
|
||||
"request_id": "3475c004-0ada-4306-9d30-d7f5efce50d2",
|
||||
"content": "彗尾的形状",
|
||||
"role": "user",
|
||||
"knowledge_references": [],
|
||||
"agent_steps": [],
|
||||
"is_completed": true,
|
||||
"mentioned_items": [],
|
||||
"images": [],
|
||||
"channel": "web",
|
||||
"created_at": "2025-08-12T14:30:39.732246+08:00",
|
||||
"updated_at": "2025-08-12T14:30:39.733277+08:00",
|
||||
"deleted_at": null
|
||||
},
|
||||
{
|
||||
"id": "9bcafbcf-a758-40af-a9a3-c4d8e0f49439",
|
||||
"session_id": "ceb9babb-1e30-41d7-817d-fd584954304b",
|
||||
"request_id": "3475c004-0ada-4306-9d30-d7f5efce50d2",
|
||||
"content": "<think>\n好的",
|
||||
"role": "assistant",
|
||||
"knowledge_references": [
|
||||
{
|
||||
"id": "c8347bef-127f-4a22-b962-edf5a75386ec",
|
||||
"content": "彗星xxx",
|
||||
"knowledge_id": "a6790b93-4700-4676-bd48-0d4804e1456b",
|
||||
"chunk_index": 0,
|
||||
"knowledge_title": "彗星.txt",
|
||||
"start_at": 0,
|
||||
"end_at": 2760,
|
||||
"seq": 0,
|
||||
"score": 4.038836479187012,
|
||||
"match_type": 3,
|
||||
"sub_chunk_id": [
|
||||
"688821f0-40bf-428e-8cb6-541531ebeb76",
|
||||
"c1e9903e-2b4d-4281-be15-0149288d45c2",
|
||||
"7d955251-3f79-4fd5-a6aa-02f81e044091"
|
||||
],
|
||||
"metadata": {},
|
||||
"chunk_type": "text",
|
||||
"parent_chunk_id": "",
|
||||
"image_info": "",
|
||||
"knowledge_filename": "彗星.txt",
|
||||
"knowledge_source": ""
|
||||
},
|
||||
{
|
||||
"id": "fa3aadee-cadb-4a84-9941-c839edc3e626",
|
||||
"content": "# 文档名称\n彗星.txt\n\n# 摘要\n彗星是由冰和尘埃构成的太阳系小天体,接近太阳时会释放气体形成彗发和彗尾。其轨道周期差异大,来源包括柯伊伯带和奥尔特云。彗星与小行星的区别逐渐模糊,部分彗星已失去挥发物质,类似小行星。目前已知彗星数量众多,且存在系外彗星。彗星在古代被视为凶兆,现代研究揭示其复杂结构与起源。",
|
||||
"knowledge_id": "a6790b93-4700-4676-bd48-0d4804e1456b",
|
||||
"chunk_index": 6,
|
||||
"knowledge_title": "彗星.txt",
|
||||
"start_at": 0,
|
||||
"end_at": 0,
|
||||
"seq": 6,
|
||||
"score": 0.6131043121858466,
|
||||
"match_type": 3,
|
||||
"sub_chunk_id": null,
|
||||
"metadata": {},
|
||||
"chunk_type": "summary",
|
||||
"parent_chunk_id": "c8347bef-127f-4a22-b962-edf5a75386ec",
|
||||
"image_info": "",
|
||||
"knowledge_filename": "彗星.txt",
|
||||
"knowledge_source": ""
|
||||
}
|
||||
],
|
||||
"agent_steps": [],
|
||||
"is_completed": true,
|
||||
"is_fallback": false,
|
||||
"agent_duration_ms": 2500,
|
||||
"channel": "web",
|
||||
"created_at": "2025-08-12T14:30:39.735108+08:00",
|
||||
"updated_at": "2025-08-12T14:31:17.829926+08:00",
|
||||
"deleted_at": null
|
||||
}
|
||||
],
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## DELETE `/messages/:session_id/:id` - 删除消息
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request DELETE 'http://localhost:8080/api/v1/messages/ceb9babb-1e30-41d7-817d-fd584954304b/9bcafbcf-a758-40af-a9a3-c4d8e0f49439' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Message deleted successfully",
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/messages/search` - 搜索历史对话
|
||||
|
||||
搜索历史对话消息,支持混合搜索、关键词搜索和向量搜索模式。
|
||||
|
||||
**请求参数**:
|
||||
- `query`: 搜索关键词(必填)
|
||||
- `mode`: 搜索模式,可选 `hybrid`、`keyword`、`vector`(可选,默认 `hybrid`)
|
||||
- `limit`: 返回结果数量(可选,默认 20)
|
||||
- `session_ids`: 限定搜索的会话ID列表(可选)
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/messages/search' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"query": "彗星的结构",
|
||||
"mode": "hybrid",
|
||||
"limit": 20,
|
||||
"session_ids": []
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"request_id": "3475c004-0ada-4306-9d30-d7f5efce50d2",
|
||||
"session_id": "ceb9babb-1e30-41d7-817d-fd584954304b",
|
||||
"session_title": "彗星知识问答",
|
||||
"query_content": "彗尾的形状",
|
||||
"answer_content": "彗尾的形状主要取决于...",
|
||||
"score": 0.85,
|
||||
"match_type": "hybrid",
|
||||
"created_at": "2025-08-12T14:30:39.732246+08:00"
|
||||
}
|
||||
],
|
||||
"total": 1
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## GET `/messages/chat-history-stats` - 获取聊天历史知识库统计
|
||||
|
||||
获取当前空间的聊天历史知识库索引统计信息。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/messages/chat-history-stats' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"enabled": true,
|
||||
"embedding_model_id": "dff7bc94-7885-4dd1-bfd5-bd96e4df2fc3",
|
||||
"knowledge_base_id": "kb-chat-00000001",
|
||||
"knowledge_base_name": "聊天历史知识库",
|
||||
"indexed_message_count": 1024,
|
||||
"has_indexed_messages": true
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,503 @@
|
||||
# 模型管理 API
|
||||
|
||||
[返回目录](./README.md)
|
||||
|
||||
模型管理接口用于维护当前空间下可用的 LLM / Embedding / Rerank / VLLM / ASR 模型配置。
|
||||
|
||||
| 方法 | 路径 | 描述 |
|
||||
| ------ | ------------------- | --------------------- |
|
||||
| GET | `/models/providers` | 获取模型服务商列表 |
|
||||
| POST | `/models` | 创建模型 |
|
||||
| GET | `/models` | 获取模型列表 |
|
||||
| GET | `/models/:id` | 获取模型详情 |
|
||||
| PUT | `/models/:id` | 更新模型 |
|
||||
| DELETE | `/models/:id` | 删除模型 |
|
||||
|
||||
## 服务商支持 (Provider Support)
|
||||
|
||||
WeKnora 支持多种主流 AI 模型服务商,在创建模型时可通过 `parameters.provider` 字段指定服务商类型以获得更好的兼容性。
|
||||
|
||||
### 支持的服务商列表
|
||||
|
||||
| 服务商标识 | 名称 | 支持的模型类型 |
|
||||
| -------------- | ---------------------------- | ------------------------------- |
|
||||
| `generic` | 自定义 (OpenAI 兼容接口) | Chat, Embedding, Rerank, VLLM |
|
||||
| `openai` | OpenAI | Chat, Embedding, Rerank, VLLM |
|
||||
| `aliyun` | 阿里云 DashScope | Chat, Embedding, Rerank, VLLM |
|
||||
| `zhipu` | 智谱 BigModel | Chat, Embedding, Rerank, VLLM |
|
||||
| `volcengine` | 火山引擎 Volcengine | Chat, Embedding, Rerank, VLLM |
|
||||
| `hunyuan` | 腾讯混元 Hunyuan | Chat, Embedding |
|
||||
| `deepseek` | DeepSeek | Chat |
|
||||
| `minimax` | MiniMax | Chat |
|
||||
| `mimo` | 小米 MiMo | Chat |
|
||||
| `siliconflow` | 硅基流动 SiliconFlow | Chat, Embedding, Rerank, VLLM |
|
||||
| `jina` | Jina | Embedding, Rerank |
|
||||
| `openrouter` | OpenRouter | Chat, VLLM |
|
||||
| `litellm` | LiteLLM (self-hosted proxy) | Chat, Embedding, VLLM |
|
||||
| `requesty` | Requesty | Chat, Embedding, VLLM |
|
||||
| `gemini` | Google Gemini | Chat |
|
||||
| `modelscope` | 魔搭 ModelScope | Chat, Embedding, VLLM |
|
||||
| `moonshot` | 月之暗面 Moonshot | Chat, VLLM |
|
||||
| `qianfan` | 百度千帆 Baidu Cloud | Chat, Embedding, Rerank, VLLM |
|
||||
| `qiniu` | 七牛云 Qiniu | Chat |
|
||||
| `longcat` | LongCat AI | Chat |
|
||||
| `gpustack` | GPUStack | Chat, Embedding, Rerank, VLLM |
|
||||
|
||||
> 实际可用的服务商以 `GET /models/providers` 返回为准。
|
||||
>
|
||||
> `litellm` 的目录默认地址是占位符 `http://your_litellm_proxy/v1`,保存前请换成实际可解析的主机名。`localhost` / `127.0.0.1` / `host.docker.internal` 默认会被 SSRF 拦截;自托管时把该主机加入 `SSRF_WHITELIST`。创建模型时 `source` 仍为 `remote`,厂商写在 `parameters.provider`。
|
||||
|
||||
## GET `/models/providers` - 获取模型服务商列表
|
||||
|
||||
根据模型类型获取支持的服务商列表及配置信息(系统级元数据,与空间无关)。
|
||||
|
||||
**查询参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ---------- | ------ | ---- | --------------------------------------------------- |
|
||||
| model_type | string | 否 | 模型类型,可选值:`chat` / `embedding` / `rerank` / `vllm` / `asr`;省略则返回全部 |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
# 获取所有服务商
|
||||
curl --location 'http://localhost:8080/api/v1/models/providers' \
|
||||
--header 'X-API-Key: your_api_key'
|
||||
|
||||
# 获取支持 Embedding 类型的服务商
|
||||
curl --location 'http://localhost:8080/api/v1/models/providers?model_type=embedding' \
|
||||
--header 'X-API-Key: your_api_key'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"value": "aliyun",
|
||||
"label": "阿里云 DashScope",
|
||||
"description": "qwen-plus, tongyi-embedding-vision-plus, qwen3-rerank, etc.",
|
||||
"defaultUrls": {
|
||||
"chat": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
"embedding": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
"rerank": "https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank"
|
||||
},
|
||||
"modelTypes": ["chat", "embedding", "rerank", "vllm"]
|
||||
},
|
||||
{
|
||||
"value": "zhipu",
|
||||
"label": "智谱 BigModel",
|
||||
"description": "glm-4.7, embedding-3, rerank, etc.",
|
||||
"defaultUrls": {
|
||||
"chat": "https://open.bigmodel.cn/api/paas/v4",
|
||||
"embedding": "https://open.bigmodel.cn/api/paas/v4/embeddings",
|
||||
"rerank": "https://open.bigmodel.cn/api/paas/v4/rerank"
|
||||
},
|
||||
"modelTypes": ["chat", "embedding", "rerank", "vllm"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/models` - 创建模型
|
||||
|
||||
为当前空间创建一个新的模型配置。
|
||||
|
||||
**参数说明(请求体)**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ----------- | ------ | ---- | --------------------------------------------------------------- |
|
||||
| name | string | 是 | 模型名称(远程模型对应服务商的 model id,本地模型为 Ollama tag)|
|
||||
| type | string | 是 | 模型类型,可选值:`KnowledgeQA` / `Embedding` / `Rerank` / `VLLM` / `ASR` |
|
||||
| source | string | 是 | 模型来源,可选值:`local` / `remote` |
|
||||
| description | string | 否 | 模型描述 |
|
||||
| parameters | object | 是 | 模型参数,详见下方 [Parameters](#parameters-模型参数) |
|
||||
|
||||
> 当 `parameters.base_url` 不为空时,后端会执行 SSRF 校验,校验失败将返回 400。
|
||||
|
||||
### 创建对话模型(KnowledgeQA)
|
||||
|
||||
**本地 Ollama 模型**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/models' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'X-API-Key: your_api_key' \
|
||||
--data '{
|
||||
"name": "qwen3:8b",
|
||||
"type": "KnowledgeQA",
|
||||
"source": "local",
|
||||
"description": "LLM Model for Knowledge QA",
|
||||
"parameters": {
|
||||
"base_url": "",
|
||||
"api_key": ""
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
**远程 API 模型(指定服务商)**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/models' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'X-API-Key: your_api_key' \
|
||||
--data '{
|
||||
"name": "qwen-plus",
|
||||
"type": "KnowledgeQA",
|
||||
"source": "remote",
|
||||
"description": "阿里云 Qwen 大模型",
|
||||
"parameters": {
|
||||
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
"api_key": "sk-your-dashscope-api-key",
|
||||
"provider": "aliyun"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### 创建嵌入模型(Embedding)
|
||||
|
||||
**本地 Ollama 模型**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/models' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'X-API-Key: your_api_key' \
|
||||
--data '{
|
||||
"name": "nomic-embed-text:latest",
|
||||
"type": "Embedding",
|
||||
"source": "local",
|
||||
"description": "Embedding Model",
|
||||
"parameters": {
|
||||
"base_url": "",
|
||||
"api_key": "",
|
||||
"embedding_parameters": {
|
||||
"dimension": 768,
|
||||
"truncate_prompt_tokens": 0
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
**远程 API 模型(阿里云 DashScope)**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/models' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'X-API-Key: your_api_key' \
|
||||
--data '{
|
||||
"name": "text-embedding-v3",
|
||||
"type": "Embedding",
|
||||
"source": "remote",
|
||||
"description": "阿里云通义千问 Embedding 模型",
|
||||
"parameters": {
|
||||
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
"api_key": "sk-your-dashscope-api-key",
|
||||
"provider": "aliyun",
|
||||
"embedding_parameters": {
|
||||
"dimension": 1024,
|
||||
"truncate_prompt_tokens": 0
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
**远程 API 模型(Jina AI)**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/models' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'X-API-Key: your_api_key' \
|
||||
--data '{
|
||||
"name": "jina-embeddings-v3",
|
||||
"type": "Embedding",
|
||||
"source": "remote",
|
||||
"description": "Jina AI Embedding 模型",
|
||||
"parameters": {
|
||||
"base_url": "https://api.jina.ai/v1",
|
||||
"api_key": "jina_your_api_key",
|
||||
"provider": "jina",
|
||||
"embedding_parameters": {
|
||||
"dimension": 1024,
|
||||
"truncate_prompt_tokens": 0
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### 创建排序模型(Rerank)
|
||||
|
||||
**远程 API 模型(阿里云 DashScope)**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/models' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'X-API-Key: your_api_key' \
|
||||
--data '{
|
||||
"name": "gte-rerank",
|
||||
"type": "Rerank",
|
||||
"source": "remote",
|
||||
"description": "阿里云 GTE Rerank 模型",
|
||||
"parameters": {
|
||||
"base_url": "https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank",
|
||||
"api_key": "sk-your-dashscope-api-key",
|
||||
"provider": "aliyun"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
**远程 API 模型(Jina AI)**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/models' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'X-API-Key: your_api_key' \
|
||||
--data '{
|
||||
"name": "jina-reranker-v2-base-multilingual",
|
||||
"type": "Rerank",
|
||||
"source": "remote",
|
||||
"description": "Jina AI Rerank 模型",
|
||||
"parameters": {
|
||||
"base_url": "https://api.jina.ai/v1",
|
||||
"api_key": "jina_your_api_key",
|
||||
"provider": "jina"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
**远程 API 模型(火山引擎 VikingDB)**:
|
||||
|
||||
火山 Rerank 使用 AK/SK 签名,不使用方舟 API Key。`api_key` 保存 Access Key ID,
|
||||
`app_secret` 保存 Secret Access Key;两项均按模型凭证加密存储。
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/models' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'X-API-Key: your_api_key' \
|
||||
--data '{
|
||||
"name": "doubao-seed-rerank",
|
||||
"type": "Rerank",
|
||||
"source": "remote",
|
||||
"description": "火山引擎托管 Rerank 模型",
|
||||
"parameters": {
|
||||
"base_url": "https://api-knowledgebase.mlp.cn-beijing.volces.com",
|
||||
"api_key": "your-volcengine-access-key-id",
|
||||
"app_secret": "your-volcengine-secret-access-key",
|
||||
"provider": "volcengine"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### 创建视觉模型(VLLM)
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/models' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'X-API-Key: your_api_key' \
|
||||
--data '{
|
||||
"name": "qwen-vl-plus",
|
||||
"type": "VLLM",
|
||||
"source": "remote",
|
||||
"description": "阿里云通义千问视觉模型",
|
||||
"parameters": {
|
||||
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
"api_key": "sk-your-dashscope-api-key",
|
||||
"provider": "aliyun"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "09c5a1d6-ee8b-4657-9a17-d3dcbd5c70cb",
|
||||
"tenant_id": 1,
|
||||
"name": "text-embedding-v3",
|
||||
"type": "Embedding",
|
||||
"source": "remote",
|
||||
"description": "阿里云通义千问 Embedding 模型",
|
||||
"parameters": {
|
||||
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
"api_key": "sk-***",
|
||||
"provider": "aliyun",
|
||||
"embedding_parameters": {
|
||||
"dimension": 1024,
|
||||
"truncate_prompt_tokens": 0
|
||||
}
|
||||
},
|
||||
"is_default": false,
|
||||
"status": "active",
|
||||
"created_at": "2025-08-12T10:39:01.454591766+08:00",
|
||||
"updated_at": "2025-08-12T10:39:01.454591766+08:00",
|
||||
"deleted_at": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## GET `/models` - 获取模型列表
|
||||
|
||||
返回当前空间下的所有模型。内置模型(`is_builtin = true`)的 `base_url` 与 `api_key` 会被清空以隐藏敏感信息。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/models' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'X-API-Key: your_api_key'
|
||||
```
|
||||
|
||||
**响应**: `data` 为数组,每个元素的字段结构同 `POST /models` 响应。内置模型的 `base_url` 与 `api_key` 字段为空字符串。
|
||||
|
||||
## GET `/models/:id` - 获取模型详情
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ---- | ------ | ---- | -------- |
|
||||
| id | string | 是 | 模型 ID |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/models/dff7bc94-7885-4dd1-bfd5-bd96e4df2fc3' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'X-API-Key: your_api_key'
|
||||
```
|
||||
|
||||
**响应**: 字段结构同 `POST /models` 响应。404 表示模型不存在。
|
||||
|
||||
## PUT `/models/:id` - 更新模型
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ---- | ------ | ---- | -------- |
|
||||
| id | string | 是 | 模型 ID |
|
||||
|
||||
**参数说明(请求体)**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ----------- | ------ | ---- | --------------------------------------------------------------- |
|
||||
| name | string | 否 | 模型名称(为空字符串时保留原值) |
|
||||
| description | string | 否 | 模型描述(始终覆盖,传空字符串会清空) |
|
||||
| type | string | 否 | 模型类型,取值同创建接口 |
|
||||
| source | string | 否 | 模型来源,取值同创建接口 |
|
||||
| parameters | object | 否 | 模型参数;`parameter_size` 由后端管理,请求中无需提供;`extra_config` 为空时会沿用旧值 |
|
||||
|
||||
> 同样会对 `parameters.base_url` 做 SSRF 校验,失败时返回 400。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request PUT 'http://localhost:8080/api/v1/models/8fdc464d-8eaa-44d4-a85b-094b28af5330' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'X-API-Key: your_api_key' \
|
||||
--data '{
|
||||
"name": "gte-rerank-v2",
|
||||
"type": "Rerank",
|
||||
"source": "remote",
|
||||
"description": "阿里云 GTE Rerank 模型 V2",
|
||||
"parameters": {
|
||||
"base_url": "https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank",
|
||||
"api_key": "sk-your-new-api-key",
|
||||
"provider": "aliyun"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**: 字段结构同 `POST /models` 响应,返回更新后的完整模型对象。
|
||||
|
||||
## DELETE `/models/:id` - 删除模型
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ---- | ------ | ---- | -------- |
|
||||
| id | string | 是 | 模型 ID |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request DELETE 'http://localhost:8080/api/v1/models/8fdc464d-8eaa-44d4-a85b-094b28af5330' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'X-API-Key: your_api_key'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Model deleted"
|
||||
}
|
||||
```
|
||||
|
||||
如果知识库、智能体或当前空间的长期记忆仍引用该模型,接口保持 HTTP 400,并返回专用错误码 `2300` 和结构化引用详情。例如:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": 2300,
|
||||
"message": "model is used by 2 knowledge base(s); reconfigure or remove those references before deleting",
|
||||
"details": {
|
||||
"knowledge_bases": [
|
||||
{"id": "kb-1", "name": "Product docs", "bindings": ["vlm_model"]},
|
||||
{"id": "kb-2", "name": "Engineering", "bindings": ["vlm_model"]}
|
||||
],
|
||||
"agents": [],
|
||||
"long_term_memory": {"bindings": []},
|
||||
"knowledge_base_total": 2,
|
||||
"agent_total": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`bindings` 是稳定的配置类型标识:知识库可能返回 `embedding_model`、`summary_model`、`image_processing_model`、`vlm_model`、`asr_model`、`wiki_synthesis_model`;智能体可能返回 `chat_model`、`rerank_model`、`vlm_model`、`asr_model`、`query_understand_model`、`follow_up_model`;长期记忆可能返回 `embedding_model`、`extract_model`。同一对象的多处引用会合并到一个 `bindings` 数组。`knowledge_bases` / `agents` 最多各返回 50 条;未截断的引用数在 `knowledge_base_total` / `agent_total`。404 表示模型不存在。
|
||||
|
||||
## 参数说明
|
||||
|
||||
### ModelType (模型类型)
|
||||
|
||||
| 值 | 前端别名 | 说明 | 用途 |
|
||||
| ----------- | ----------- | ------------ | ------------------------------ |
|
||||
| KnowledgeQA | `chat` | 对话模型 | 知识库问答、对话生成 |
|
||||
| Embedding | `embedding` | 嵌入模型 | 文本向量化、知识库检索 |
|
||||
| Rerank | `rerank` | 排序模型 | 检索结果重排序、相关性优化 |
|
||||
| VLLM | `vllm` | 视觉语言模型 | 多模态分析、图文理解 |
|
||||
| ASR | `asr` | 语音识别模型 | 音频转写 |
|
||||
|
||||
> 创建/更新接口请求体的 `type` 字段使用第一列的后端枚举值(如 `KnowledgeQA`);`GET /models/providers?model_type=` 查询参数使用第二列的前端别名(如 `chat`)。
|
||||
|
||||
### ModelSource (模型来源)
|
||||
|
||||
| 值 | 说明 | 配置要求 |
|
||||
| -------- | ---------- | -------------------------------- |
|
||||
| local | 本地模型 | 需要已安装 Ollama 并拉取模型 |
|
||||
| remote | 远程 API | 需要提供 `base_url` 和 `api_key` |
|
||||
|
||||
### Parameters (模型参数)
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| -------------------- | ----------------- | ---- | ---------------------------------------------------------- |
|
||||
| base_url | string | 否 | API 服务地址;远程模型必填,会经过 SSRF 校验 |
|
||||
| api_key | string | 否 | API 密钥;远程模型必填,存储时使用 AES-256 加密 |
|
||||
| provider | string | 否 | 服务商标识(见上方支持列表),用于选择特定的 API 适配器 |
|
||||
| interface_type | string | 否 | 接口风格标识(OpenAI 兼容请留空) |
|
||||
| embedding_parameters | object | 否 | Embedding 模型专用参数,见下方 |
|
||||
| parameter_size | string | 否 | 模型参数规模(如 `7B`/`13B`/`70B`),通常由后端写入 |
|
||||
| extra_config | object<string,string> | 否 | 服务商特定的额外配置 |
|
||||
| custom_headers | object<string,string> | 否 | 调用上游 API 时附加的自定义 HTTP 头;保留头会被忽略 |
|
||||
| supports_vision | bool | 否 | 模型是否支持图像/多模态输入 |
|
||||
|
||||
### EmbeddingParameters (嵌入参数)
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ---------------------- | ---- | ---- | ------------------------------- |
|
||||
| dimension | int | 否 | 向量维度(如 768、1024) |
|
||||
| truncate_prompt_tokens | int | 否 | 截断 Token 数(0 表示不截断) |
|
||||
@@ -0,0 +1,477 @@
|
||||
# 会话管理 API
|
||||
|
||||
[返回目录](./README.md)
|
||||
|
||||
会话(Session)是纯粹的对话容器,仅存储基础信息(标题、描述、置顶状态等)。所有与知识库、模型、检索策略相关的配置均在查询时由 Custom Agent 提供,不再存储在会话中。
|
||||
|
||||
| 方法 | 路径 | 描述 |
|
||||
| ------ | ------------------------------------------ | ----------------------------- |
|
||||
| POST | `/sessions` | 创建会话 |
|
||||
| DELETE | `/sessions/batch` | 批量删除会话 |
|
||||
| GET | `/sessions/:id` | 获取会话详情 |
|
||||
| GET | `/sessions` | 获取当前空间的会话列表 |
|
||||
| PUT | `/sessions/:id` | 更新会话 |
|
||||
| DELETE | `/sessions/:id` | 删除会话 |
|
||||
| DELETE | `/sessions/:id/messages` | 清空会话消息 |
|
||||
| POST | `/sessions/:session_id/generate_title` | 生成会话标题 |
|
||||
| POST | `/sessions/:session_id/stop` | 停止生成 |
|
||||
| POST | `/sessions/:session_id/pin` | 置顶会话 |
|
||||
| DELETE | `/sessions/:id/pin` | 取消置顶会话 |
|
||||
| GET | `/sessions/continue-stream/:session_id` | 继续未完成的流式响应 |
|
||||
|
||||
> **路由命名说明**:置顶接口的 POST 与 DELETE 使用了不同的路径参数名(POST 用 `:session_id`,DELETE 用 `:id`)。这是由于 gin 路由器为每个 HTTP 方法维护独立的 radix tree,且既有树中的通配符命名不同,必须保留以避免注册时的 `wildcard conflicts` panic。两者语义上都指会话 ID。
|
||||
|
||||
## POST `/sessions` - 创建会话
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/sessions' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"title": "我的新对话",
|
||||
"description": "关于 AI 的讨论"
|
||||
}'
|
||||
```
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 描述 |
|
||||
| ------------- | ------ | ---- | -------- |
|
||||
| `title` | string | 否 | 会话标题 |
|
||||
| `description` | string | 否 | 会话描述 |
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "411d6b70-9a85-4d03-bb74-aab0fd8bd12f",
|
||||
"title": "我的新对话",
|
||||
"description": "关于 AI 的讨论",
|
||||
"tenant_id": 1,
|
||||
"user_id": "u-001",
|
||||
"is_pinned": false,
|
||||
"created_at": "2026-03-27T12:26:19.611616+08:00",
|
||||
"updated_at": "2026-03-27T12:26:19.611616+08:00",
|
||||
"deleted_at": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 通过 API-Key 调用时 `user_id` 可能为空,此时会话以空间级可见。
|
||||
|
||||
## DELETE `/sessions/batch` - 批量删除会话
|
||||
|
||||
支持两种模式:按 ID 列表批量删除,或删除当前空间的所有会话。
|
||||
|
||||
**请求 - 按 ID 列表删除**:
|
||||
|
||||
```curl
|
||||
curl --location --request DELETE 'http://localhost:8080/api/v1/sessions/batch' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"ids": [
|
||||
"411d6b70-9a85-4d03-bb74-aab0fd8bd12f",
|
||||
"ceb9babb-1e30-41d7-817d-fd584954304b"
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
**请求 - 删除所有会话**:
|
||||
|
||||
```curl
|
||||
curl --location --request DELETE 'http://localhost:8080/api/v1/sessions/batch' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"delete_all": true
|
||||
}'
|
||||
```
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 描述 |
|
||||
| ------------ | -------- | ---- | ---------------------------------------------------------- |
|
||||
| `ids` | string[] | 否 | 要删除的会话 ID 列表(`delete_all` 为 `false` 时必填) |
|
||||
| `delete_all` | bool | 否 | 设为 `true` 时删除当前空间的所有会话,忽略 `ids` 字段 |
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Sessions deleted successfully"
|
||||
}
|
||||
```
|
||||
|
||||
`delete_all=true` 时 `message` 为 `"All sessions deleted successfully"`。
|
||||
|
||||
## GET `/sessions/:id` - 获取会话详情
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/sessions/ceb9babb-1e30-41d7-817d-fd584954304b' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 描述 |
|
||||
| ---- | ------ | ---- | ------- |
|
||||
| `id` | string | 是 | 会话 ID |
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "ceb9babb-1e30-41d7-817d-fd584954304b",
|
||||
"title": "模型优化策略",
|
||||
"description": "",
|
||||
"tenant_id": 1,
|
||||
"user_id": "u-001",
|
||||
"is_pinned": true,
|
||||
"pinned_at": "2026-04-01T09:12:33.123456+08:00",
|
||||
"created_at": "2026-03-27T10:24:38.308596+08:00",
|
||||
"updated_at": "2026-03-27T10:25:41.317761+08:00",
|
||||
"deleted_at": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
会话不存在时返回 `404`。
|
||||
|
||||
## GET `/sessions` - 获取当前空间的会话列表
|
||||
|
||||
获取当前空间的会话列表,支持分页、关键字搜索、按来源 / Agent 过滤。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/sessions?page=1&page_size=10&keyword=AI&source=web' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**查询参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 描述 |
|
||||
| ----------- | ------ | ---- | ----------------------------------------------------------------- |
|
||||
| `page` | int | 否 | 页码(默认 1) |
|
||||
| `page_size` | int | 否 | 每页数量(默认 10) |
|
||||
| `keyword` | string | 否 | 按标题模糊匹配(ILIKE `%keyword%`) |
|
||||
| `source` | string | 否 | 来源过滤:`web`(无 IM 映射)或 IM 平台名,如 `feishu`、`wechat`、`slack` |
|
||||
| `agent_id` | string | 否 | 按 Agent 过滤(仅对 IM 会话生效) |
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": "411d6b70-9a85-4d03-bb74-aab0fd8bd12f",
|
||||
"title": "我的新对话",
|
||||
"description": "",
|
||||
"tenant_id": 1,
|
||||
"user_id": "u-001",
|
||||
"is_pinned": true,
|
||||
"pinned_at": "2026-04-01T09:12:33.123456+08:00",
|
||||
"created_at": "2026-03-27T12:26:19.611616+08:00",
|
||||
"updated_at": "2026-03-27T12:26:19.611616+08:00",
|
||||
"deleted_at": null,
|
||||
"im_platform": "feishu",
|
||||
"im_chat_id": "oc_xxx",
|
||||
"im_agent_id": "agent-001"
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"page": 1,
|
||||
"page_size": 10
|
||||
}
|
||||
```
|
||||
|
||||
> 列表项始终包含置顶状态字段,IM 来源相关字段(`im_platform`、`im_chat_id`、`im_thread_id`、`im_user_id`、`im_agent_id`、`im_channel_id`)仅对 IM 创建的会话填充,Web 会话省略。
|
||||
|
||||
## PUT `/sessions/:id` - 更新会话
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request PUT 'http://localhost:8080/api/v1/sessions/411d6b70-9a85-4d03-bb74-aab0fd8bd12f' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"title": "WeKnora 技术讨论",
|
||||
"description": "关于 WeKnora 架构的讨论"
|
||||
}'
|
||||
```
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 描述 |
|
||||
| ---- | ------ | ---- | ------- |
|
||||
| `id` | string | 是 | 会话 ID |
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 描述 |
|
||||
| ------------- | ------ | ---- | -------- |
|
||||
| `title` | string | 否 | 会话标题 |
|
||||
| `description` | string | 否 | 会话描述 |
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "411d6b70-9a85-4d03-bb74-aab0fd8bd12f",
|
||||
"title": "WeKnora 技术讨论",
|
||||
"description": "关于 WeKnora 架构的讨论",
|
||||
"tenant_id": 1,
|
||||
"user_id": "u-001",
|
||||
"is_pinned": false,
|
||||
"created_at": "2026-03-27T12:26:19.611616+08:00",
|
||||
"updated_at": "2026-03-27T14:20:56.738424+08:00",
|
||||
"deleted_at": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
会话不存在时返回 `404`。
|
||||
|
||||
## DELETE `/sessions/:id` - 删除会话
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request DELETE 'http://localhost:8080/api/v1/sessions/411d6b70-9a85-4d03-bb74-aab0fd8bd12f' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 描述 |
|
||||
| ---- | ------ | ---- | ------- |
|
||||
| `id` | string | 是 | 会话 ID |
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Session deleted successfully"
|
||||
}
|
||||
```
|
||||
|
||||
## DELETE `/sessions/:id/messages` - 清空会话消息
|
||||
|
||||
删除会话中的所有消息,同时清除 LLM 上下文和聊天历史知识库条目。会话本身保留。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request DELETE 'http://localhost:8080/api/v1/sessions/ceb9babb-1e30-41d7-817d-fd584954304b/messages' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 描述 |
|
||||
| ---- | ------ | ---- | ------- |
|
||||
| `id` | string | 是 | 会话 ID |
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Session messages cleared successfully"
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/sessions/:session_id/generate_title` - 生成会话标题
|
||||
|
||||
根据消息内容自动生成会话标题。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/sessions/ceb9babb-1e30-41d7-817d-fd584954304b/generate_title' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你好,我想了解关于人工智能的知识"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "人工智能是计算机科学的一个分支..."
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 描述 |
|
||||
| ------------ | ------ | ---- | ------- |
|
||||
| `session_id` | string | 是 | 会话 ID |
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 描述 |
|
||||
| ---------- | --------- | ---- | ---------------------------- |
|
||||
| `messages` | Message[] | 是 | 用作标题生成上下文的消息列表 |
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": "人工智能基础知识"
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/sessions/:session_id/stop` - 停止生成
|
||||
|
||||
停止当前正在进行的助手回复生成任务。后端会向流中追加一条 `stop` 事件,由活跃的 SSE 处理协程感知并发起取消。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/sessions/7c966c74-610e-4516-8d5b-05e14b2e4ee0/stop' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"message_id": "ebbf7e53-dfe6-44d5-882f-36a4104910b5"
|
||||
}'
|
||||
```
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 描述 |
|
||||
| ------------ | ------ | ---- | ------- |
|
||||
| `session_id` | string | 是 | 会话 ID |
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 描述 |
|
||||
| ------------ | ------ | ---- | ----------------------- |
|
||||
| `message_id` | string | 是 | 要停止生成的助手消息 ID |
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Generation stopped"
|
||||
}
|
||||
```
|
||||
|
||||
若消息已完成(无需停止):
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Message already completed"
|
||||
}
|
||||
```
|
||||
|
||||
> 消息不属于当前会话返回 `403`;消息或会话不存在返回 `404`。
|
||||
|
||||
## POST `/sessions/:session_id/pin` - 置顶会话
|
||||
|
||||
将指定会话置顶(用户维度)。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request POST 'http://localhost:8080/api/v1/sessions/ceb9babb-1e30-41d7-817d-fd584954304b/pin' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 描述 |
|
||||
| ------------ | ------ | ---- | ------- |
|
||||
| `session_id` | string | 是 | 会话 ID |
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"is_pinned": true
|
||||
}
|
||||
```
|
||||
|
||||
会话不存在或对当前用户不可见时返回 `404`。
|
||||
|
||||
## DELETE `/sessions/:id/pin` - 取消置顶会话
|
||||
|
||||
取消指定会话的置顶。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request DELETE 'http://localhost:8080/api/v1/sessions/ceb9babb-1e30-41d7-817d-fd584954304b/pin' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 描述 |
|
||||
| ---- | ------ | ---- | ------- |
|
||||
| `id` | string | 是 | 会话 ID |
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"is_pinned": false
|
||||
}
|
||||
```
|
||||
|
||||
> 同上,`POST /pin` 与 `DELETE /pin` 路径参数名不同,但语义上都是会话 ID。
|
||||
|
||||
## GET `/sessions/continue-stream/:session_id` - 继续未完成的流式响应
|
||||
|
||||
用于在 SSE 连接断开后重新连接正在进行的流式响应:先回放该消息已产生的所有事件,再继续推送后续事件,直至 `complete`。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 描述 |
|
||||
| ------------ | ------ | ---- | ------- |
|
||||
| `session_id` | string | 是 | 会话 ID |
|
||||
|
||||
**查询参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 描述 |
|
||||
| ------------ | ------ | ---- | ------------------------------------------------------------------------------- |
|
||||
| `message_id` | string | 是 | 从 `/messages/:session_id/load` 接口中获取的 `is_completed` 为 `false` 的消息 ID |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/sessions/continue-stream/ceb9babb-1e30-41d7-817d-fd584954304b?message_id=b8b90eeb-7dd5-4cf9-81c6-5ebcbd759451' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应格式**:
|
||||
|
||||
服务器端事件流(Server-Sent Events),事件结构与 `/knowledge-chat/:session_id`、`/agent-chat/:session_id` 返回结果一致。若该消息当前在流中已无事件返回 `404 No stream events found`;若消息记录不存在返回 `404 Incomplete message not found`。
|
||||
@@ -0,0 +1,305 @@
|
||||
# Skills API
|
||||
|
||||
[返回目录](./README.md)
|
||||
|
||||
| 方法 | 路径 | 描述 |
|
||||
| ---- | --------- | ------------------ |
|
||||
| GET | `/skills` | 获取预装 Skills 列表 |
|
||||
| POST | `/sandbox-configs/{id}/skills` | 安装技能(zip 上传或托管平台 source) |
|
||||
| POST | `/sandbox-configs/{id}/skills/{skillId}/reinstall` | 用已保存的安装包重试安装 |
|
||||
| POST | `/sandbox-configs/{id}/skills/{skillId}/stop` | 停止卡住的安装 |
|
||||
| GET | `/sandbox-configs/{id}/skills/{skillId}/files` | 列出已安装技能的文件 |
|
||||
| GET | `/sandbox-configs/{id}/skills/{skillId}/files/content` | 读取已安装技能中的单个文件 |
|
||||
| PATCH | `/sandbox-configs/{id}/skills/{skillId}` | 启用/停用技能,或设置空间级环境变量 |
|
||||
| GET | `/me/env-vars` | 列出自己的环境变量与各技能的声明 |
|
||||
| PUT | `/me/env-vars/skill` | 设置自己在某个技能上的变量值 |
|
||||
| DELETE | `/me/env-vars/skill` | 删除自己在某个技能上的变量值 |
|
||||
| PUT | `/me/env-vars/sandbox` | 设置自己在某个沙箱配置上的变量值 |
|
||||
| DELETE | `/me/env-vars/sandbox` | 删除自己在某个沙箱配置上的变量值 |
|
||||
|
||||
## GET `/skills` - 获取预装 Skills 列表
|
||||
|
||||
获取系统中所有预装的智能体技能列表。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/skills' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"name": "web_search",
|
||||
"description": "搜索互联网获取最新信息"
|
||||
},
|
||||
{
|
||||
"name": "code_interpreter",
|
||||
"description": "执行代码并返回结果"
|
||||
},
|
||||
{
|
||||
"name": "image_generation",
|
||||
"description": "根据文本描述生成图片"
|
||||
}
|
||||
],
|
||||
"skills_available": true,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
当系统未配置 Skills 时,`skills_available` 返回 `false`,`data` 为空数组:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [],
|
||||
"skills_available": false,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/sandbox-configs/{id}/skills` - 安装技能
|
||||
|
||||
把技能安装到指定沙箱配置的镜像上。安装会启动沙箱并运行数分钟,本接口只负责受理,随后通过
|
||||
`GET /sandbox-configs/{id}/skills/{skillId}/install-events` 跟随进度。
|
||||
|
||||
两种请求体二选一:
|
||||
|
||||
### 1. 上传 zip(multipart)
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/sandbox-configs/{id}/skills' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--form 'file=@"skill.zip"'
|
||||
```
|
||||
|
||||
### 2. 从托管平台安装(JSON)
|
||||
|
||||
`source` 只接受一种明确写法,不会根据下载结果猜测:
|
||||
|
||||
| 输入 | 含义 |
|
||||
| --- | --- |
|
||||
| `@owner/slug`、`@owner/slug@1.2.0` | ClawHub(默认 registry) |
|
||||
| `my-skill`、`my-skill@1.2.0`(不含 `/`) | ClawHub slug |
|
||||
| `https://clawhub.ai/skills-sh/owner/repo/slug`、`skills-sh:owner/repo/slug` | ClawHub 上的 skills.sh 联邦条目(经 ClawHub install API 解析到钉死的 GitHub commit;仓库内路径可能比 URL slug 更深) |
|
||||
| `https://clawhub.ai/...`、`https://skillhub.cn/...`、自托管 SkillHub 页面 | 对应 registry |
|
||||
| `https://github.com/...`、`https://gitlab.com/...`、`https://skills.sh/...` | Git 托管;`skills.sh` 的 `owner/repo/slug` 页面同样走 ClawHub resolver |
|
||||
| `https://…/foo.zip` 或 `…/SKILL.md` | 直接下载 |
|
||||
|
||||
`owner/slug`(无 `@`、无 URL)会 400:它既是 ClawHub id 也是 GitHub 仓库,请改成 `@owner/slug` 或粘贴完整链接。
|
||||
|
||||
来源必须可匿名读取:服务端不会为这次下载附带任何凭据,因此私有仓库/私有 registry 需要先自行导出 zip 再上传。
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/sandbox-configs/{id}/skills' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{"source":"@owner/slug"}'
|
||||
```
|
||||
|
||||
**响应**(202):
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"skill_id": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/sandbox-configs/{id}/skills/{skillId}/reinstall` - 重试安装
|
||||
|
||||
用服务端已保存的安装包重新跑一遍安装,无需重新上传 zip 或重新提供 source。适用于安装失败的原因与安装包本身无关的情况:沙箱不可达、依赖源超时、安装过程被中断等。
|
||||
|
||||
与安装接口一样只负责受理,进度同样通过
|
||||
`GET /sandbox-configs/{id}/skills/{skillId}/install-events` 跟随。技能会复用同一个 `skill_id`,不会产生新记录。
|
||||
|
||||
已经在当前镜像中正常服务、且安装包未变的技能会被跳过,不会重复构建快照。若该技能的安装包已不在存储中,返回 400,此时只能重新上传。
|
||||
|
||||
```curl
|
||||
curl --location --request POST \
|
||||
'http://localhost:8080/api/v1/sandbox-configs/{id}/skills/{skillId}/reinstall' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应**(202):
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"skill_id": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/sandbox-configs/{id}/skills/{skillId}/stop` - 停止安装
|
||||
|
||||
中止进行中的安装,之后可以重试或卸载。服务重启后安装行可能一直停在 `installing` 且没有存活进程,本接口会立刻改写该行,不必等 stuck-run reaper。卸载不受影响。
|
||||
|
||||
状态变为 `failed`,错误为「安装已停止」。
|
||||
|
||||
```curl
|
||||
curl --location --request POST \
|
||||
'http://localhost:8080/api/v1/sandbox-configs/{id}/skills/{skillId}/stop' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应**(200):
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "...",
|
||||
"status": "failed",
|
||||
"error": "安装已停止"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
若技能不是 `installing`(且不是已经 `failed`),返回 400。已经 `failed` 的停止是幂等成功。
|
||||
|
||||
## GET `/sandbox-configs/{id}/skills/{skillId}/files` - 列出技能文件
|
||||
|
||||
返回该技能存档里的文件路径与大小。路径相对技能根目录(`SKILL.md` 所在目录),不启动沙箱。
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/sandbox-configs/{id}/skills/{skillId}/files' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{ "path": "SKILL.md", "size": 412 },
|
||||
{ "path": "scripts/extract.py", "size": 1280 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## GET `/sandbox-configs/{id}/skills/{skillId}/files/content` - 读取技能文件
|
||||
|
||||
`path` 为技能根目录相对路径。文本以 UTF-8 返回;较小的图片为 base64;其它二进制文件不返回正文,并设置 `binary: true`。
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/sandbox-configs/{id}/skills/{skillId}/files/content?path=SKILL.md' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"path": "SKILL.md",
|
||||
"size": 412,
|
||||
"encoding": "utf-8",
|
||||
"media_type": "text/markdown",
|
||||
"content": "---\nname: pdf-tools\n..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 技能的环境变量
|
||||
|
||||
技能安装时会声明自己需要哪些环境变量(名称、说明、是否必填)。变量的**值**分两层,执行时按「本人的值 → 空间级值」的顺序取,取不到必填项则拒绝执行:
|
||||
|
||||
| 层级 | 谁能写 | 作用范围 | 接口 |
|
||||
| --- | --- | --- | --- |
|
||||
| 空间级 | Admin+ | 该空间所有人 | `PATCH /sandbox-configs/{id}/skills/{skillId}` |
|
||||
| 个人级 | 任何登录成员 | 仅本人 | `PUT /me/env-vars/skill` |
|
||||
|
||||
任何接口都**不会**回读已保存的值,只返回 `unset` / `workspace` / `user` 三种状态。清空一个值用 DELETE,而不是写入空字符串——「没填」和「不需要」是两种状态。
|
||||
|
||||
个人级的值按**调用身份**存放,而不是按用户 ID。用 API Key 驱动的调用与网页登录是不同身份:在网页 Settings 里填的值不会作用于 API Key 发起的执行,反之亦然。集成方若通过 API Key 运行需要凭据的技能,请让管理员配置空间级值。
|
||||
|
||||
### PATCH `/sandbox-configs/{id}/skills/{skillId}` - 更新技能
|
||||
|
||||
`enabled` 与 `envs` 都是可选的,可只送其一,但不能都不送(400)。`envs` 只写技能已声明的名称,未声明的名称会被忽略而不是报错;值为空字符串表示清空该值并保留声明。
|
||||
|
||||
```curl
|
||||
curl --location --request PATCH \
|
||||
'http://localhost:8080/api/v1/sandbox-configs/{id}/skills/{skillId}' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{"enabled":true,"envs":{"TAVILY_API_KEY":"tvly-xxxxx"}}'
|
||||
```
|
||||
|
||||
### GET `/me/env-vars` - 列出自己的环境变量
|
||||
|
||||
按沙箱配置分组,返回本人的配置级变量,以及该配置下每个已启用技能声明的变量。`source` 表示这次执行实际会用哪一层的值。
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/me/env-vars' \
|
||||
--header 'Authorization: Bearer <token>'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"sandbox_config_id": "cfg-1",
|
||||
"sandbox_config_name": "默认沙箱",
|
||||
"description": "日常对话用的沙箱",
|
||||
"vars": [
|
||||
{ "name": "HTTP_PROXY", "source": "user", "updated_at": "2026-08-27T10:00:00Z" }
|
||||
],
|
||||
"skills": [
|
||||
{
|
||||
"skill_id": "sk-1",
|
||||
"skill_name": "web-search",
|
||||
"description": "通过 Tavily 检索网页",
|
||||
"vars": [
|
||||
{ "name": "TAVILY_API_KEY", "description": "Tavily 搜索密钥", "required": true, "source": "workspace" },
|
||||
{ "name": "REGION", "source": "unset" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### PUT / DELETE `/me/env-vars/skill` - 设置或删除自己在技能上的值
|
||||
|
||||
`name` 必须是该技能声明过的名称,否则 400。删除后该技能重新回落到空间级值(若有)。
|
||||
|
||||
```curl
|
||||
curl --location --request PUT 'http://localhost:8080/api/v1/me/env-vars/skill' \
|
||||
--header 'Authorization: Bearer <token>' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{"skill_id":"sk-1","name":"TAVILY_API_KEY","value":"tvly-xxxxx"}'
|
||||
```
|
||||
|
||||
```curl
|
||||
curl --location --request DELETE 'http://localhost:8080/api/v1/me/env-vars/skill' \
|
||||
--header 'Authorization: Bearer <token>' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{"skill_id":"sk-1","name":"TAVILY_API_KEY"}'
|
||||
```
|
||||
|
||||
删除一个本就没设过的值返回 404。
|
||||
|
||||
### PUT / DELETE `/me/env-vars/sandbox` - 设置或删除自己的配置级变量
|
||||
|
||||
配置级变量不依附于任何技能,名称自定,会注入本人在该沙箱配置上的每一次技能脚本与 shell 命令。`WEKNORA_` 前缀与 `PATH` 等保留名会被拒绝。
|
||||
|
||||
```curl
|
||||
curl --location --request PUT 'http://localhost:8080/api/v1/me/env-vars/sandbox' \
|
||||
--header 'Authorization: Bearer <token>' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{"sandbox_config_id":"cfg-1","name":"HTTP_PROXY","value":"http://127.0.0.1:7890"}'
|
||||
```
|
||||
@@ -0,0 +1,341 @@
|
||||
# Storage Backend API
|
||||
|
||||
[返回目录](./README.md)
|
||||
|
||||
存储后端(StorageBackend)API 用于管理空间的对象/文件存储实例。一个空间可以注册多个存储实例(`local`、`minio`、`cos`、`tos`、`s3`、`oss`、`ks3`、`obs`),并将不同知识库绑定到不同实例;空间维度还有一个默认实例(`default_storage_backend_id`),未显式绑定的新知识库使用该默认实例。
|
||||
|
||||
接口同时管理用户创建的实例(`source: "user"`)以及从环境变量快照生成的只读实例(`source: "env"`)。存储后端 CRUD 需要 **Admin+** 角色;对 API Key 需具备 `manage_storage_backends` capability(或 full-access)。
|
||||
|
||||
| 方法 | 路径 | 描述 | 最低权限 |
|
||||
| ------ | --------------------------------- | -------------------------------------- | -------- |
|
||||
| GET | `/storage-backends/types` | 获取 `STORAGE_ALLOW_LIST` 允许的存储类型 | Viewer+ |
|
||||
| POST | `/storage-backends/test` | 使用原始配置测试连通性(不落库) | Admin+ |
|
||||
| POST | `/storage-backends` | 创建存储实例 | Admin+ |
|
||||
| GET | `/storage-backends` | 获取存储实例列表 | Viewer+ |
|
||||
| GET | `/storage-backends/:id` | 获取存储实例详情 | Viewer+ |
|
||||
| PUT | `/storage-backends/:id` | 更新存储实例(名称/凭据/状态) | Admin+ |
|
||||
| DELETE | `/storage-backends/:id` | 删除存储实例(软删除) | Admin+ |
|
||||
| POST | `/storage-backends/:id/test` | 测试已保存实例的连通性 | Admin+ |
|
||||
| PUT | `/storage-backends/:id/default` | 设为空间默认存储实例 | Admin+ |
|
||||
|
||||
> 所有响应中的敏感字段(`access_key_id`、`secret_access_key`)都会被掩码。更新时若提交掩码占位符,则保留库中原有的真实凭据,不会被占位符覆盖。
|
||||
|
||||
## 存储配置字段(`config`)
|
||||
|
||||
不同 provider 使用同一套归一化的配置对象,按 provider 取用其中的子集:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ------------------- | ------- | ------------------------------------------------------------- |
|
||||
| mode | string | MinIO 模式:`docker`(复用环境变量凭据)或 `remote` |
|
||||
| endpoint | string | 对象存储 endpoint(COS 使用 region,不需要 endpoint) |
|
||||
| region | string | 区域 |
|
||||
| access_key_id | string | 访问密钥 ID(COS 对应 SecretID);响应中掩码 |
|
||||
| secret_access_key | string | 访问密钥 Secret(COS 对应 SecretKey);响应中掩码 |
|
||||
| bucket_name | string | Bucket 名称 |
|
||||
| path_prefix | string | 对象前缀,必须为相对路径,禁止 `/` 开头或 `..` 上跳 |
|
||||
| app_id | string | 腾讯云 COS AppID |
|
||||
| use_ssl | boolean | 是否使用 SSL |
|
||||
| force_path_style | boolean | S3 是否使用 path-style 寻址 |
|
||||
| use_temp_bucket | boolean | OSS 是否使用临时 bucket |
|
||||
| temp_bucket_name | string | 临时 bucket 名称 |
|
||||
| temp_region | string | 临时 bucket 区域 |
|
||||
|
||||
> `endpoint`、`region`、`bucket_name`、`path_prefix` 决定对象的物理位置,**创建后不可变更**(更新时会被拒绝);如需迁移请使用存储迁移流程。凭据可通过更新单独轮换。
|
||||
|
||||
## GET `/storage-backends/types` - 获取允许的存储类型
|
||||
|
||||
返回 `STORAGE_ALLOW_LIST` 允许的 provider 列表,可用于前端动态表单生成。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/storage-backends/types' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": ["local", "minio", "cos", "s3"]
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/storage-backends/test` - 使用原始配置测试连通性
|
||||
|
||||
用前端表单中尚未保存的配置执行一次连通性测试,不会写入数据库。
|
||||
|
||||
**参数说明(请求体)**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| -------- | ------ | ---- | ------------------------------------------- |
|
||||
| name | string | 是 | 实例显示名 |
|
||||
| provider | string | 是 | 存储类型,取自 `/storage-backends/types` |
|
||||
| config | object | 否 | 该 provider 对应的存储配置字段 |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request POST 'http://localhost:8080/api/v1/storage-backends/test' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name": "s3-hot",
|
||||
"provider": "s3",
|
||||
"config": {
|
||||
"endpoint": "https://s3.example.com",
|
||||
"region": "ap-test-1",
|
||||
"access_key_id": "AKID",
|
||||
"secret_access_key": "SECRET",
|
||||
"bucket_name": "weknora"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
**响应(成功)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
**响应(失败)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "连接被拒绝,请确认服务已启动且端口正确"
|
||||
}
|
||||
```
|
||||
|
||||
> 测试失败时 HTTP 状态码仍为 `200`,错误信息通过 `success: false` + `error` 返回;`error` 已脱敏,不会泄漏内部主机名、IP、端口或 TLS 细节。
|
||||
|
||||
## POST `/storage-backends` - 创建存储实例
|
||||
|
||||
为当前空间创建一个新的存储实例。创建前会先校验配置、执行 SSRF 校验(本地存储与 docker 模式 MinIO 除外),并执行一次连通性测试;任一环节失败都会返回 `400`。同一空间内实例名称不允许重复。
|
||||
|
||||
**参数说明(请求体)**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| -------- | ------ | ---- | ------------------------------------------- |
|
||||
| name | string | 是 | 实例显示名(空间内唯一) |
|
||||
| provider | string | 是 | 存储类型,取自 `/storage-backends/types` |
|
||||
| config | object | 否 | 该 provider 对应的存储配置字段 |
|
||||
| status | string | 否 | `active`(默认)或 `disabled` |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/storage-backends' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name": "s3-hot",
|
||||
"provider": "s3",
|
||||
"config": {
|
||||
"endpoint": "https://s3.example.com",
|
||||
"region": "ap-test-1",
|
||||
"access_key_id": "AKID",
|
||||
"secret_access_key": "SECRET",
|
||||
"bucket_name": "weknora",
|
||||
"path_prefix": "prod"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
**响应** (201):
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "s3-hot",
|
||||
"provider": "s3",
|
||||
"config": {
|
||||
"endpoint": "https://s3.example.com",
|
||||
"region": "ap-test-1",
|
||||
"access_key_id": "***",
|
||||
"secret_access_key": "***",
|
||||
"bucket_name": "weknora",
|
||||
"path_prefix": "prod"
|
||||
},
|
||||
"source": "user",
|
||||
"status": "active",
|
||||
"legacy_alias": false,
|
||||
"created_at": "2026-07-15T10:00:00Z",
|
||||
"updated_at": "2026-07-15T10:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## GET `/storage-backends` - 获取存储实例列表
|
||||
|
||||
返回当前空间的所有存储实例(凭据已掩码),并在顶层返回空间默认实例 id。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/storage-backends' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "s3-hot",
|
||||
"provider": "s3",
|
||||
"config": { "endpoint": "https://s3.example.com", "access_key_id": "***", "secret_access_key": "***", "bucket_name": "weknora" },
|
||||
"source": "user",
|
||||
"status": "active",
|
||||
"legacy_alias": false
|
||||
}
|
||||
],
|
||||
"default_storage_backend_id": "550e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
```
|
||||
|
||||
## GET `/storage-backends/:id` - 获取存储实例详情
|
||||
|
||||
根据 ID 获取当前空间下的单个存储实例,凭据已掩码。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ---- | ------ | ---- | ------------- |
|
||||
| id | string | 是 | 存储实例 ID |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/storage-backends/550e8400-e29b-41d4-a716-446655440000' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
## PUT `/storage-backends/:id` - 更新存储实例
|
||||
|
||||
更新实例的可变字段(`name`、凭据、`status`)。`provider` 与物理位置字段(`endpoint`、`region`、`bucket_name`、`path_prefix`)不可变更,尝试更改会返回 `400`。环境变量来源(`source: "env"`)的实例只读,不可更新。更新同样会执行校验与连通性测试。
|
||||
|
||||
> 若 `access_key_id` / `secret_access_key` 提交为掩码占位符(`***`),则保留库中原有真实凭据。
|
||||
|
||||
**禁用保护**:将当前为默认实例、或仍有知识库绑定的实例改为 `disabled` 会被拒绝(`400`)。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ---- | ------ | ---- | ------------- |
|
||||
| id | string | 是 | 存储实例 ID |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request PUT 'http://localhost:8080/api/v1/storage-backends/550e8400-e29b-41d4-a716-446655440000' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name": "s3-hot-renamed",
|
||||
"provider": "s3",
|
||||
"config": {
|
||||
"access_key_id": "***",
|
||||
"secret_access_key": "NEW_SECRET"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## DELETE `/storage-backends/:id` - 删除存储实例
|
||||
|
||||
对存储实例执行软删除。以下情况删除会被拒绝(`400`):实例是空间默认实例、仍有知识库绑定、环境变量来源(只读)、或为 legacy 别名(旧文件路径可能仍引用它)。删除在事务中执行;PostgreSQL 上对目标行加行锁以避免并发绑定竞态。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ---- | ------ | ---- | ------------- |
|
||||
| id | string | 是 | 存储实例 ID |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request DELETE 'http://localhost:8080/api/v1/storage-backends/550e8400-e29b-41d4-a716-446655440000' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应(成功)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/storage-backends/:id/test` - 测试已保存实例的连通性
|
||||
|
||||
对已保存的存储实例用其存储的凭据执行一次连通性测试。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ---- | ------ | ---- | ------------- |
|
||||
| id | string | 是 | 存储实例 ID |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request POST 'http://localhost:8080/api/v1/storage-backends/550e8400-e29b-41d4-a716-446655440000/test' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应(成功)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
> 与 `/storage-backends/test` 一致,测试失败时 HTTP 状态码仍为 `200`,错误经脱敏后通过 `success: false` + `error` 返回。
|
||||
|
||||
## PUT `/storage-backends/:id/default` - 设为空间默认实例
|
||||
|
||||
将某个存储实例标记为空间默认实例。仅 `active` 状态的实例可以设为默认。未显式绑定存储实例的新知识库将使用默认实例。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ---- | ------ | ---- | ------------- |
|
||||
| id | string | 是 | 存储实例 ID |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request PUT 'http://localhost:8080/api/v1/storage-backends/550e8400-e29b-41d4-a716-446655440000/default' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## 环境变量存储实例
|
||||
|
||||
通过 `STORAGE_TYPE` 等环境变量配置的存储会以只读实例(`source: "env"`、`legacy_alias: true`)形式参与实例解析,使 env-only 部署与用户管理的实例走同一套解析路径。这类实例在每次启动时按环境变量刷新,且不可通过 API 更新或删除。
|
||||
|
||||
## 错误码
|
||||
|
||||
| HTTP 状态码 | 含义 |
|
||||
| ----------- | ---------------------------------------------------------------- |
|
||||
| 400 | 请求参数错误、校验失败、SSRF 校验失败、连通性测试失败、尝试更改不可变字段、修改只读实例、删除受保护实例、禁用被引用实例、设为默认时实例非 active |
|
||||
| 401 | 未认证(缺少空间上下文或 API Key) |
|
||||
| 403 | 权限不足(需 Admin+ 或 API Key `manage_storage_backends` capability) |
|
||||
| 404 | 存储实例不存在 |
|
||||
| 409 | 同名存储实例已存在 |
|
||||
@@ -0,0 +1,255 @@
|
||||
# 系统管理 API
|
||||
|
||||
[返回目录](./README.md)
|
||||
|
||||
| 方法 | 路径 | 描述 |
|
||||
| ------ | --------------------------------- | ---------------------- |
|
||||
| GET | `/system/capabilities` | 获取部署能力清单 |
|
||||
| GET | `/system/info` | 获取系统信息 |
|
||||
| GET | `/system/parser-engines` | 获取解析引擎列表 |
|
||||
| POST | `/system/parser-engines/check` | 检查解析引擎可用性 |
|
||||
| POST | `/system/docreader/reconnect` | 重连文档解析服务 |
|
||||
| GET | `/system/storage-engine-status` | 获取存储引擎状态 |
|
||||
| POST | `/system/storage-engine-check` | 检查存储引擎连通性 |
|
||||
|
||||
## GET `/system/capabilities` - 获取部署能力清单
|
||||
|
||||
返回当前部署版本,以及各功能模块是否已在后端注册对应路由。`supported: false` 表示 SPA 应隐藏相关入口;字段缺失或接口不可用时不应据此清空整个菜单(fail-open),但 Lite 版会始终将 `organizations` 标记为不支持。
|
||||
|
||||
**权限**:Viewer+(租户成员);任意有效 API Key 可读(`apiKeyAny`)。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/system/capabilities' \
|
||||
--header 'Authorization: Bearer <token>' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"edition": "standard",
|
||||
"capabilities": {
|
||||
"organizations": { "supported": true },
|
||||
"agents": { "supported": true },
|
||||
"integrations.im": { "supported": true },
|
||||
"integrations.embed": { "supported": false, "reason": "route_not_registered" },
|
||||
"integrations.api": { "supported": true },
|
||||
"settings.mcp": { "supported": true },
|
||||
"settings.websearch": { "supported": true },
|
||||
"settings.vectorstore": { "supported": true },
|
||||
"settings.storage": { "supported": true },
|
||||
"settings.sandbox": { "supported": true }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Lite 版示例(共享空间不可用):
|
||||
|
||||
```json
|
||||
{
|
||||
"capabilities": {
|
||||
"organizations": {
|
||||
"supported": false,
|
||||
"reason": "not_supported_in_lite"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## GET `/system/info` - 获取系统信息
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/system/info' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"version": "1.2.0",
|
||||
"edition": "community",
|
||||
"commit_id": "a1b2c3d",
|
||||
"build_time": "2025-08-12T08:00:00Z",
|
||||
"go_version": "go1.21.5",
|
||||
"keyword_index_engine": "bleve",
|
||||
"vector_store_engine": "milvus",
|
||||
"graph_database_engine": "neo4j",
|
||||
"minio_enabled": true,
|
||||
"db_version": "20250810_001"
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## GET `/system/parser-engines` - 获取解析引擎列表
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/system/parser-engines' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"name": "docreader",
|
||||
"label": "DocReader",
|
||||
"description": "高精度文档解析引擎",
|
||||
"available": true
|
||||
},
|
||||
{
|
||||
"name": "tika",
|
||||
"label": "Apache Tika",
|
||||
"description": "通用文档解析引擎",
|
||||
"available": false
|
||||
}
|
||||
],
|
||||
"connected": true,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/system/parser-engines/check` - 检查解析引擎可用性
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/system/parser-engines/check' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"addr": "http://docreader:8000"
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"name": "docreader",
|
||||
"label": "DocReader",
|
||||
"description": "高精度文档解析引擎",
|
||||
"available": true
|
||||
}
|
||||
],
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/system/docreader/reconnect` - 重连文档解析服务
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/system/docreader/reconnect' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"addr": "http://docreader:8000"
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## GET `/system/storage-engine-status` - 获取存储引擎状态
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/system/storage-engine-status' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"engines": [
|
||||
{
|
||||
"name": "minio",
|
||||
"available": true,
|
||||
"description": "MinIO 对象存储"
|
||||
},
|
||||
{
|
||||
"name": "cos",
|
||||
"available": false,
|
||||
"description": "腾讯云 COS 对象存储"
|
||||
},
|
||||
{
|
||||
"name": "s3",
|
||||
"available": false,
|
||||
"description": "AWS S3 对象存储"
|
||||
},
|
||||
{
|
||||
"name": "oss",
|
||||
"available": false,
|
||||
"description": "阿里云 OSS 对象存储"
|
||||
}
|
||||
],
|
||||
"minio_env_available": true
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/system/storage-engine-check` - 检查存储引擎连通性
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/system/storage-engine-check' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"provider": "minio",
|
||||
"minio": {
|
||||
"endpoint": "localhost:9000",
|
||||
"access_key": "minioadmin",
|
||||
"secret_key": "minioadmin",
|
||||
"bucket": "weknora",
|
||||
"use_ssl": false
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"ok": true,
|
||||
"message": "连接成功",
|
||||
"bucket_created": false
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
# 标签管理 API
|
||||
|
||||
[返回目录](./README.md)
|
||||
|
||||
| 方法 | 路径 | 描述 |
|
||||
| ------ | ------------------------------------- | ------------------------ |
|
||||
| GET | `/knowledge-bases/:id/tags` | 获取知识库标签列表 |
|
||||
| POST | `/knowledge-bases/:id/tags` | 创建标签 |
|
||||
| PUT | `/knowledge-bases/:id/tags/:tag_id` | 更新标签 |
|
||||
| DELETE | `/knowledge-bases/:id/tags/:tag_id` | 删除标签 |
|
||||
|
||||
## GET `/knowledge-bases/:id/tags` - 获取知识库标签列表
|
||||
|
||||
**查询参数**:
|
||||
- `page`: 页码(默认 1)
|
||||
- `page_size`: 每页条数(默认 20)
|
||||
- `keyword`: 标签名称关键字搜索(可选)
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/knowledge-bases/kb-00000001/tags?page=1&page_size=10' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"total": 2,
|
||||
"page": 1,
|
||||
"page_size": 10,
|
||||
"data": [
|
||||
{
|
||||
"id": "tag-00000001",
|
||||
"tenant_id": 1,
|
||||
"knowledge_base_id": "kb-00000001",
|
||||
"name": "技术文档",
|
||||
"color": "#1890ff",
|
||||
"sort_order": 1,
|
||||
"created_at": "2025-08-12T10:00:00+08:00",
|
||||
"updated_at": "2025-08-12T10:00:00+08:00",
|
||||
"knowledge_count": 5,
|
||||
"chunk_count": 120
|
||||
},
|
||||
{
|
||||
"id": "tag-00000002",
|
||||
"tenant_id": 1,
|
||||
"knowledge_base_id": "kb-00000001",
|
||||
"name": "常见问题",
|
||||
"color": "#52c41a",
|
||||
"sort_order": 2,
|
||||
"created_at": "2025-08-12T10:00:00+08:00",
|
||||
"updated_at": "2025-08-12T10:00:00+08:00",
|
||||
"knowledge_count": 3,
|
||||
"chunk_count": 45
|
||||
}
|
||||
]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/knowledge-bases/:id/tags` - 创建标签
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ---- | ------ | ----------- |
|
||||
| id | string | 知识库 ID |
|
||||
|
||||
**参数说明(请求体)**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ---------- | ------ | ---- | ------------------------ |
|
||||
| name | string | 是 | 标签名(同库内唯一) |
|
||||
| color | string | 否 | 标签颜色(CSS 颜色字符串) |
|
||||
| sort_order | int | 否 | 排序值(数值越小越靠前) |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/knowledge-bases/kb-00000001/tags' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name": "产品手册",
|
||||
"color": "#faad14",
|
||||
"sort_order": 3
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "tag-00000003",
|
||||
"tenant_id": 1,
|
||||
"knowledge_base_id": "kb-00000001",
|
||||
"name": "产品手册",
|
||||
"color": "#faad14",
|
||||
"sort_order": 3,
|
||||
"created_at": "2025-08-12T11:00:00+08:00",
|
||||
"updated_at": "2025-08-12T11:00:00+08:00"
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## PUT `/knowledge-bases/:id/tags/:tag_id` - 更新标签
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ------ | ------ | ----------- |
|
||||
| id | string | 知识库 ID |
|
||||
| tag_id | string | 标签 ID |
|
||||
|
||||
**参数说明(请求体)**: 同创建接口,所有字段均可选;未传则保留原值。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request PUT 'http://localhost:8080/api/v1/knowledge-bases/kb-00000001/tags/tag-00000003' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name": "产品手册更新",
|
||||
"color": "#ff4d4f"
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "tag-00000003",
|
||||
"tenant_id": 1,
|
||||
"knowledge_base_id": "kb-00000001",
|
||||
"name": "产品手册更新",
|
||||
"color": "#ff4d4f",
|
||||
"sort_order": 3,
|
||||
"created_at": "2025-08-12T11:00:00+08:00",
|
||||
"updated_at": "2025-08-12T11:30:00+08:00"
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## DELETE `/knowledge-bases/:id/tags/:tag_id` - 删除标签
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ------ | ------ | -------- |
|
||||
| id | string | 知识库 ID |
|
||||
| tag_id | string | 标签 ID |
|
||||
|
||||
**查询参数**:
|
||||
|
||||
| 字段 | 类型 | 默认 | 说明 |
|
||||
| ----- | ------- | ----- | --------------------------------------------- |
|
||||
| force | boolean | false | 设置为 `true` 时强制删除(即使标签被引用) |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request DELETE 'http://localhost:8080/api/v1/knowledge-bases/kb-00000001/tags/tag-00000003?force=true' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,649 @@
|
||||
# 空间管理 API
|
||||
|
||||
[返回目录](./README.md)
|
||||
|
||||
包含两组接口:
|
||||
- 空间 CRUD(`/tenants`、`/tenants/:id`):当前认证用户对自己所属空间进行管理;跨空间访问需要管理员权限。
|
||||
- 跨空间接口(`/tenants/all`、`/tenants/search`):**需要服务端启用 `EnableCrossTenantAccess` 且当前用户具备 `CanAccessAllTenants` 权限**,否则返回 403。
|
||||
- 空间 KV 配置(`/tenants/kv/:key`):当前空间级别的通用配置项,**`tenant_id` 从认证上下文中获取,不在 URL 中传入**。
|
||||
|
||||
| 方法 | 路径 | 描述 |
|
||||
| ------ | -------------------------- | ------------------------------------------------- |
|
||||
| GET | `/tenants/all` | 获取所有空间列表(需跨空间权限) |
|
||||
| GET | `/tenants/search` | 分页搜索空间(需跨空间权限) |
|
||||
| POST | `/tenants` | 创建新空间 |
|
||||
| GET | `/tenants/:id` | 获取指定空间信息 |
|
||||
| PUT | `/tenants/:id` | 更新空间信息 |
|
||||
| DELETE | `/tenants/:id` | 删除空间 |
|
||||
| GET | `/tenants/:id/api-keys` | 列出空间 API Key(Owner) |
|
||||
| POST | `/tenants/:id/api-keys` | 创建带角色的 API Key(Owner) |
|
||||
| DELETE | `/tenants/:id/api-keys/:key_id` | 吊销指定 API Key(Owner) |
|
||||
| GET | `/tenants/:id/api-principal-config` | 获取 API Key 用户身份配置(Owner) |
|
||||
| PUT | `/tenants/:id/api-principal-config` | 更新 API Key 用户身份配置(Owner) |
|
||||
| GET | `/tenants` | 获取当前用户可见的空间列表 |
|
||||
| GET | `/tenants/kv/:key` | 获取当前空间的 KV 配置(空间由认证上下文确定) |
|
||||
| PUT | `/tenants/kv/:key` | 更新当前空间的 KV 配置(空间由认证上下文确定) |
|
||||
|
||||
## GET `/tenants/all` - 获取所有空间列表
|
||||
|
||||
获取系统中所有空间列表,需要跨空间权限。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/tenants/all' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'X-API-Key: sk-An7_t_izCKFIJ4iht9Xjcjnj_MC48ILvwezEDki9ScfIa7KA'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"id": 10001,
|
||||
"name": "weknora-1",
|
||||
"description": "weknora workspaces 1",
|
||||
"status": "active",
|
||||
"business": "wechat",
|
||||
"created_at": "2025-08-11T20:37:28.39698+08:00",
|
||||
"updated_at": "2025-08-11T20:37:28.405693+08:00"
|
||||
},
|
||||
{
|
||||
"id": 10002,
|
||||
"name": "weknora-2",
|
||||
"description": "weknora workspaces 2",
|
||||
"status": "active",
|
||||
"business": "wechat",
|
||||
"created_at": "2025-08-11T20:52:58.05679+08:00",
|
||||
"updated_at": "2025-08-11T20:52:58.060495+08:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## GET `/tenants/search` - 搜索空间
|
||||
|
||||
按关键词搜索空间,需要跨空间权限。
|
||||
|
||||
**查询参数**:
|
||||
- `keyword`: 搜索关键词(可选)
|
||||
- `tenant_id`: 按空间ID筛选(可选)
|
||||
- `page`: 页码(默认 1)
|
||||
- `page_size`: 每页条数(默认 20)
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/tenants/search?keyword=weknora&page=1&page_size=10' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'X-API-Key: sk-An7_t_izCKFIJ4iht9Xjcjnj_MC48ILvwezEDki9ScfIa7KA'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"id": 10002,
|
||||
"name": "weknora",
|
||||
"description": "weknora workspaces",
|
||||
"status": "active",
|
||||
"business": "wechat",
|
||||
"created_at": "2025-08-11T20:52:58.05679+08:00",
|
||||
"updated_at": "2025-08-11T20:52:58.060495+08:00"
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"page": 1,
|
||||
"page_size": 10
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/tenants` - 创建新空间
|
||||
|
||||
创建一个新的空间。**默认不会**自动发放 API Key;请在创建后通过 `POST /tenants/:id/api-keys` 创建密钥。从旧版本升级时,原有 `tenants.api_key` 会迁移到 `tenant_api_keys` 表并继续可用,直至被吊销。
|
||||
|
||||
> **兼容旧行为(可选)**:如需恢复旧版「创建空间即下发默认 API Key」的行为,可将系统设置 `tenant.auto_create_api_key` 置为 `true`(或设置环境变量 `WEKNORA_TENANT_AUTO_CREATE_API_KEY=true`)。开启后,创建空间会自动生成一个 `full_access` 权限的 API Key,并在响应体 `data.api_key` 中返回其明文 token(仅本次创建响应返回,请妥善保存)。默认 `false`。
|
||||
|
||||
**参数说明(请求体)**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ------------------------------------------------------ |
|
||||
| name | string | 是 | 空间名称 |
|
||||
| description | string | 否 | 空间描述 |
|
||||
| business | string | 否 | 业务标识(如 `wechat`) |
|
||||
| retriever_engines | object | 否 | 检索引擎组合配置(`engines` 数组:每项含 `retriever_type` 与 `retriever_engine_type`) |
|
||||
| storage_quota | int | 否 | 存储配额(字节) |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/tenants' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name": "weknora",
|
||||
"description": "weknora workspaces",
|
||||
"business": "wechat",
|
||||
"retriever_engines": {
|
||||
"engines": [
|
||||
{
|
||||
"retriever_type": "keywords",
|
||||
"retriever_engine_type": "postgres"
|
||||
},
|
||||
{
|
||||
"retriever_type": "vector",
|
||||
"retriever_engine_type": "postgres"
|
||||
}
|
||||
]
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**(默认,不含 API Key):
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": 10000,
|
||||
"name": "weknora",
|
||||
"description": "weknora workspaces",
|
||||
"status": "active",
|
||||
"retriever_engines": {
|
||||
"engines": [
|
||||
{
|
||||
"retriever_engine_type": "postgres",
|
||||
"retriever_type": "keywords"
|
||||
},
|
||||
{
|
||||
"retriever_engine_type": "postgres",
|
||||
"retriever_type": "vector"
|
||||
}
|
||||
]
|
||||
},
|
||||
"business": "wechat",
|
||||
"storage_quota": 10737418240,
|
||||
"storage_used": 0,
|
||||
"created_at": "2025-08-11T20:37:28.396980093+08:00",
|
||||
"updated_at": "2025-08-11T20:37:28.396980301+08:00",
|
||||
"deleted_at": null
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
当开启 `tenant.auto_create_api_key`(或 `WEKNORA_TENANT_AUTO_CREATE_API_KEY=true`)时,响应的 `data` 中会额外包含 `api_key` 字段(`full_access` 密钥的明文 token):
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": 10000,
|
||||
"name": "weknora",
|
||||
"description": "weknora workspaces",
|
||||
"api_key": "sk-aaLRAgvCRJcmtiL2vLMeB1FB5UV0Q-qB7DlTE1pJ9KA93XZG",
|
||||
"status": "active",
|
||||
"business": "wechat",
|
||||
"storage_quota": 10737418240,
|
||||
"storage_used": 0,
|
||||
"created_at": "2025-08-11T20:37:28.396980093+08:00",
|
||||
"updated_at": "2025-08-11T20:37:28.396980301+08:00",
|
||||
"deleted_at": null
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## GET `/tenants/:id` - 获取指定空间信息
|
||||
|
||||
获取指定 ID 的空间详情。只能访问自己所属空间;访问其他空间需要跨空间权限,否则返回 403。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ---- | ---- | ------- |
|
||||
| id | int | 空间 ID |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/tenants/10000' \
|
||||
--header 'X-API-Key: sk-aaLRAgvCRJcmtiL2vLMeB1FB5UV0Q-qB7DlTE1pJ9KA93XZG' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": 10000,
|
||||
"name": "weknora",
|
||||
"description": "weknora workspaces",
|
||||
"api_key": "sk-aaLRAgvCRJcmtiL2vLMeB1FB5UV0Q-qB7DlTE1pJ9KA93XZG",
|
||||
"status": "active",
|
||||
"retriever_engines": {
|
||||
"engines": [
|
||||
{
|
||||
"retriever_engine_type": "postgres",
|
||||
"retriever_type": "keywords"
|
||||
},
|
||||
{
|
||||
"retriever_engine_type": "postgres",
|
||||
"retriever_type": "vector"
|
||||
}
|
||||
]
|
||||
},
|
||||
"business": "wechat",
|
||||
"storage_quota": 10737418240,
|
||||
"storage_used": 0,
|
||||
"created_at": "2025-08-11T20:37:28.39698+08:00",
|
||||
"updated_at": "2025-08-11T20:37:28.405693+08:00",
|
||||
"deleted_at": null
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## PUT `/tenants/:id` - 更新空间信息
|
||||
|
||||
更新指定空间的基础信息。访问规则同 `GET /tenants/:id`。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ---- | ---- | ------- |
|
||||
| id | int | 空间 ID |
|
||||
|
||||
**参数说明(请求体)**: 与 `POST /tenants` 相同字段;未传字段保持原值。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request PUT 'http://localhost:8080/api/v1/tenants/10000' \
|
||||
--header 'X-API-Key: sk-aaLRAgvCRJcmtiL2vLMeB1FB5UV0Q-qB7DlTE1pJ9KA93XZG' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name": "weknora new",
|
||||
"description": "weknora workspaces new",
|
||||
"status": "active",
|
||||
"retriever_engines": {
|
||||
"engines": [
|
||||
{
|
||||
"retriever_engine_type": "postgres",
|
||||
"retriever_type": "keywords"
|
||||
},
|
||||
{
|
||||
"retriever_engine_type": "postgres",
|
||||
"retriever_type": "vector"
|
||||
}
|
||||
]
|
||||
},
|
||||
"business": "wechat",
|
||||
"storage_quota": 10737418240
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": 10000,
|
||||
"name": "weknora new",
|
||||
"description": "weknora workspaces new",
|
||||
"api_key": "sk-aaLRAgvCRJcmtiL2vLMeB1FB5UV0Q-qB7DlTE1pJ9KA93XZG",
|
||||
"status": "active",
|
||||
"retriever_engines": {
|
||||
"engines": [
|
||||
{
|
||||
"retriever_engine_type": "postgres",
|
||||
"retriever_type": "keywords"
|
||||
},
|
||||
{
|
||||
"retriever_engine_type": "postgres",
|
||||
"retriever_type": "vector"
|
||||
}
|
||||
]
|
||||
},
|
||||
"business": "wechat",
|
||||
"storage_quota": 10737418240,
|
||||
"storage_used": 0,
|
||||
"created_at": "2025-08-11T20:37:28.39698+08:00",
|
||||
"updated_at": "2025-08-11T20:49:02.13421034+08:00",
|
||||
"deleted_at": null
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## DELETE `/tenants/:id` - 删除空间
|
||||
|
||||
删除指定空间。访问规则同 `GET /tenants/:id`。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ---- | ---- | ------- |
|
||||
| id | int | 空间 ID |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request DELETE 'http://localhost:8080/api/v1/tenants/10000' \
|
||||
--header 'X-API-Key: sk-aaLRAgvCRJcmtiL2vLMeB1FB5UV0Q-qB7DlTE1pJ9KA93XZG' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Workspace deleted successfully",
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## API Key 管理(`tenant_api_keys`)
|
||||
|
||||
自 scoped API Key 改造后,密钥以独立记录存储,支持:
|
||||
|
||||
- **role**:`viewer`(只读 + 语义检索 POST)、`contributor`(知识库写入)、`admin`(空间级管理,不含 `/api-keys` 管理面)
|
||||
- **knowledge_base_ids**:可选,将 Key 限制在指定知识库
|
||||
- **吊销**:`DELETE /tenants/:id/api-keys/:key_id`
|
||||
- **过期**:创建时可选 `expires_at_unix`
|
||||
|
||||
空间 Key 固定绑定创建时的空间。路由级 capability 鉴权与 KB 访问守卫会在 `X-API-Key` 认证后继续强制执行。
|
||||
|
||||
### 平台 API Key
|
||||
|
||||
系统管理员可在“系统管理 → 平台 API Key”创建不绑定单一空间的 Key。平台 Key 默认可以选择任意存在的空间,但每项操作仍必须具备对应 capability;平台 Key 不支持 `full_access`。
|
||||
|
||||
- 管理接口:`GET/POST /system/admin/api-keys`、`DELETE /system/admin/api-keys/:key_id`,仅人类 SystemAdmin 会话可调用,平台 Key 不能创建或吊销其他平台 Key。
|
||||
- 调用普通空间 API 时必须同时传 `X-Tenant-ID: <空间 ID>`;服务端解析目标空间后继续复用原有空间 Context、路由 capability 和知识库范围检查。
|
||||
- 调用明确开放的 `/system/admin/*` 控制面接口时不需要 `X-Tenant-ID`,需要 `system_*` capability。
|
||||
- 平台 Key 明文仅在创建响应的 `data.token` 返回一次;列表仅返回脱敏值。
|
||||
|
||||
```bash
|
||||
curl 'http://localhost:8080/api/v1/knowledge-bases' \
|
||||
-H 'X-API-Key: <platform-api-key>' \
|
||||
-H 'X-Tenant-ID: 10000'
|
||||
```
|
||||
|
||||
平台 capability:
|
||||
|
||||
| capability | 权限 |
|
||||
| --- | --- |
|
||||
| `system_tenants_read` | 列出、搜索、查看全部空间 |
|
||||
| `system_tenants_manage` | 创建、更新、删除空间以及应用全局空间配置 |
|
||||
| `system_settings_read` | 读取系统设置 |
|
||||
| `system_settings_manage` | 更新、重置系统设置 |
|
||||
| `system_runtime_read` | 查看运行时队列和任务 |
|
||||
| `system_runtime_manage` | 重试、立即执行、取消、删除运行时任务 |
|
||||
| `system_audit_read` | 读取平台审计日志 |
|
||||
|
||||
平台 Key 也可以携带现有空间 capability,例如 `retrieve`、`ingest`、`manage_kbs`;这些能力作用于请求中 `X-Tenant-ID` 指定的空间。
|
||||
|
||||
## API Key Principal:隔离边界与安全说明
|
||||
|
||||
`api-principal-config` 控制 `X-API-Key` 请求如何映射为终端 **Principal**。请先理解以下边界,再选择模式。
|
||||
|
||||
### Principal 隔离范围(当前实现)
|
||||
|
||||
Principal **仅**用于按终端用户隔离以下能力:
|
||||
|
||||
- **对话 Session**(创建、列表、读取按外部用户分开;`仅空间` 模式仍共用空间级 Session)
|
||||
- **MCP OAuth** 访问令牌(同一空间下不同外部用户各自授权,token 互不共用)
|
||||
- 对话内 MCP OAuth 提示、MCP 工具审批等与终端用户绑定的流程
|
||||
|
||||
Principal **不会**缩小 API Key 的 HTTP 路由权限:路由访问由 Key 的 `role` 控制;空间内 RBAC 角色与 `role` 一致。知识库、Agent 等资源的细粒度访问另受 KB 守卫约束。
|
||||
|
||||
### 模式与安全假设
|
||||
|
||||
| mode | 适用场景 | 安全假设 |
|
||||
| ---- | -------- | -------- |
|
||||
| `tenant` | 无 per-user MCP 需求 | 全空间共用一个 MCP OAuth 身份 |
|
||||
| `direct_header` | 仅可信服务端到服务端 | 用户 ID 来自调用方请求头,**可被持有 API Key 的任意调用方伪造**(冒充其他外部用户并共用/劫持其 MCP OAuth 授权)。面向终端用户或不可信客户端时**禁止**使用;若必须使用,请开启 `require_direct_header` 并确保 API Key 仅保存在可信后端 |
|
||||
| `signed_token` | 面向终端用户的集成(**推荐**) | 由业务后端使用 `hmac_secret` 为外部用户签发短期 HS256 JWT;无效或缺失 token 返回 401,**不回退**为空间级 Principal |
|
||||
|
||||
`direct_header` 模式下,若未携带用户 ID 请求头:`require_direct_header=false` 时回退为空间级 Principal;`require_direct_header=true` 时返回 401。
|
||||
|
||||
## GET `/tenants/:id/api-principal-config` - 获取 API Key 用户身份配置
|
||||
|
||||
返回空间级 API Key 请求如何映射为终端 Principal 的配置。**需要 Owner 权限**。
|
||||
|
||||
**响应字段**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ---- | ---- | ---- |
|
||||
| mode | string | `tenant` / `direct_header` / `signed_token` |
|
||||
| direct_header_name | string | 直接传用户 ID 时的请求头名,默认 `X-External-User-ID` |
|
||||
| signed_token_header_name | string | 签名 token 模式请求头名,默认 `X-External-User-Token` |
|
||||
| require_direct_header | bool | `direct_header` 模式下是否强制要求用户 ID 请求头 |
|
||||
| has_hmac_secret | bool | 是否已配置 HMAC secret(不返回明文) |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/tenants/10000/api-principal-config' \
|
||||
--header 'Authorization: Bearer <token>'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"mode": "signed_token",
|
||||
"direct_header_name": "X-External-User-ID",
|
||||
"signed_token_header_name": "X-External-User-Token",
|
||||
"require_direct_header": false,
|
||||
"has_hmac_secret": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## PUT `/tenants/:id/api-principal-config` - 更新 API Key 用户身份配置
|
||||
|
||||
更新 API Key 请求的 Principal 映射方式。**需要 Owner 权限**。
|
||||
|
||||
**请求体**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ---- | ---- | ---- |
|
||||
| mode | string | 必填,`tenant` / `direct_header` / `signed_token` |
|
||||
| direct_header_name | string | 可选 |
|
||||
| signed_token_header_name | string | 可选 |
|
||||
| require_direct_header | bool | 可选,`direct_header` 模式下缺 header 是否 401 |
|
||||
| hmac_secret | string | 可选,`signed_token` 模式 HMAC 密钥;省略则保留现有值 |
|
||||
|
||||
`signed_token` 模式首次启用时必须提供 `hmac_secret`。
|
||||
|
||||
外部用户 JWT 要求:HS256 签名、`aud=weknora`、包含 `sub` 与 `tenant_id`、有效期不超过 24 小时。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request PUT 'http://localhost:8080/api/v1/tenants/10000/api-principal-config' \
|
||||
--header 'Authorization: Bearer <token>' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"mode": "direct_header",
|
||||
"direct_header_name": "X-External-User-ID",
|
||||
"require_direct_header": true
|
||||
}'
|
||||
```
|
||||
|
||||
## GET `/tenants` - 获取空间列表
|
||||
|
||||
返回当前认证上下文对应的空间(普通用户为单条;管理员仍只返回自身空间)。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/tenants' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"id": 10002,
|
||||
"name": "weknora",
|
||||
"description": "weknora workspaces",
|
||||
"api_key": "sk-An7_t_izCKFIJ4iht9Xjcjnj_MC48ILvwezEDki9ScfIa7KA",
|
||||
"status": "active",
|
||||
"retriever_engines": {
|
||||
"engines": [
|
||||
{
|
||||
"retriever_engine_type": "postgres",
|
||||
"retriever_type": "keywords"
|
||||
},
|
||||
{
|
||||
"retriever_engine_type": "postgres",
|
||||
"retriever_type": "vector"
|
||||
}
|
||||
]
|
||||
},
|
||||
"business": "wechat",
|
||||
"storage_quota": 10737418240,
|
||||
"storage_used": 0,
|
||||
"created_at": "2025-08-11T20:52:58.05679+08:00",
|
||||
"updated_at": "2025-08-11T20:52:58.060495+08:00",
|
||||
"deleted_at": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## GET `/tenants/kv/:key` - 获取空间 KV 配置
|
||||
|
||||
获取当前空间的 KV 配置项。**空间 ID 从认证上下文中获取**(即由 `X-API-Key` / Bearer Token 对应的空间决定),URL 中不需要也不接受 tenant_id。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ---- | ------ | ---------------------------------------------- |
|
||||
| key | string | 配置键名(见下方支持的 key 列表,不支持的键返回 400) |
|
||||
|
||||
**支持的 key 值**:
|
||||
|
||||
| key | 说明 |
|
||||
| ---------------------- | ----------------------------- |
|
||||
| `agent-config` | Agent 配置(最大迭代次数、温度、System Prompt、可用工具等) |
|
||||
| `web-search-config` | 网页搜索配置 |
|
||||
| `conversation-config` | 普通模式会话/对话配置 |
|
||||
| `prompt-templates` | 系统提示词模板(只读,按用户语言本地化) |
|
||||
| `parser-engine-config` | 解析引擎配置(如 MinerU) |
|
||||
| `storage-engine-config`| 存储引擎配置(Local/MinIO/COS) |
|
||||
| `chat-history-config` | 聊天历史索引配置 |
|
||||
| `retrieval-config` | 全局检索配置 |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/tenants/kv/agent-config' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应(以 `agent-config` 为例)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"max_iterations": 10,
|
||||
"allowed_tools": ["knowledge_search", "web_search"],
|
||||
"temperature": 0.3,
|
||||
"system_prompt": "...",
|
||||
"use_custom_system_prompt": false,
|
||||
"available_tools": [
|
||||
{ "name": "knowledge_search", "label": "知识库检索", "description": "..." }
|
||||
],
|
||||
"available_placeholders": [
|
||||
{ "name": "web_search_status", "label": "联网搜索状态", "description": "..." }
|
||||
]
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
失败时(不支持的键):
|
||||
|
||||
```json
|
||||
{ "success": false, "error": "unsupported key" }
|
||||
```
|
||||
|
||||
## PUT `/tenants/kv/:key` - 更新空间 KV 配置
|
||||
|
||||
更新当前空间的 KV 配置项。**空间 ID 从认证上下文中获取**,请求体结构按 `key` 不同而异。`prompt-templates` 为只读,不支持 PUT。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ---- | ------ | ----------------------------- |
|
||||
| key | string | 配置键名(见 GET 接口的支持列表,`prompt-templates` 除外) |
|
||||
|
||||
**请求(以 `agent-config` 为例)**:
|
||||
|
||||
```curl
|
||||
curl --location --request PUT 'http://localhost:8080/api/v1/tenants/kv/agent-config' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"max_iterations": 20,
|
||||
"temperature": 0.3,
|
||||
"system_prompt": ""
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"max_iterations": 20,
|
||||
"allowed_tools": ["knowledge_search", "web_search"],
|
||||
"temperature": 0.3,
|
||||
"system_prompt": "",
|
||||
"use_custom_system_prompt": false
|
||||
},
|
||||
"message": "Agent configuration updated successfully",
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
**约束**:
|
||||
|
||||
- `agent-config`: `max_iterations` 为正整数时上限 100,`-1` 表示不限制;`temperature` 取值范围 `[0, 2]`。
|
||||
- `web-search-config`: `max_results` 取值范围 `[1, 50]`。
|
||||
- `conversation-config`: 包含多项阈值校验(如 `keyword_threshold` / `vector_threshold` ∈ `[0, 1]`,`rerank_threshold` ∈ `[-10, 10]`,`temperature` ∈ `[0, 2]`,`max_completion_tokens` ∈ `[1, 100000]` 等)。
|
||||
- `retrieval-config`: `embedding_top_k` / `rerank_top_k` ∈ `[0, 200]`;阈值范围同上。
|
||||
- `storage-engine-config`: `default_provider` 必须在 `STORAGE_ALLOW_LIST` 允许的列表内。
|
||||
- `chat-history-config`: 启用且设置了 `embedding_model_id` 而尚未关联知识库时,会自动创建一个隐藏知识库并将其 ID 写入配置。
|
||||
|
||||
## 空间邀请(邮箱邀请已注册用户)
|
||||
|
||||
`POST /tenants/:id/invitations` 通过邮箱邀请**已注册**用户。全局开关 `tenant.auto_accept_invitation`(环境变量 `WEKNORA_TENANT_AUTO_ACCEPT_INVITATION`,默认 `false`)控制行为:
|
||||
|
||||
| 开关 | 行为 | 成功响应 `data` 形状 |
|
||||
| ---- | ---- | -------------------- |
|
||||
| `false` | 创建 pending 邀请,受邀人须在 `/me/invitations` 接受 | `TenantInvitation`(含 `id`、`status: pending`) |
|
||||
| `true` | 直接写入 `tenant_members`,并 reconcile 已有 pending 行 | `TenantMember`(含 `user_id`、`status: active`) |
|
||||
|
||||
开启 auto-accept 时,无默认空间的受邀人会将该空间设为 home tenant(与手动接受邀请一致)。SPA 通过 `GET /auth/me` 的 `capabilities.auto_accept_invitation` 感知开关,无需读取系统设置 API。
|
||||
@@ -0,0 +1,437 @@
|
||||
# Vector Store API
|
||||
|
||||
[返回目录](./README.md)
|
||||
|
||||
向量存储(VectorStore)API 用于管理空间的向量数据库连接配置,支持 Elasticsearch、PostgreSQL、Qdrant、Milvus、Weaviate、Tencent VectorDB、SQLite 等引擎。接口同时管理用户在 DB 中创建的配置(`source: "user"`)以及通过 `RETRIEVE_DRIVER` 环境变量配置的虚拟存储(`source: "env"`,只读)。
|
||||
|
||||
| 方法 | 路径 | 描述 |
|
||||
| ------ | ---------------------------- | -------------------------------- |
|
||||
| GET | `/vector-stores/types` | 获取支持的引擎类型及字段元数据 |
|
||||
| POST | `/vector-stores/test` | 使用原始凭据测试连接(不落库) |
|
||||
| POST | `/vector-stores` | 创建向量存储 |
|
||||
| GET | `/vector-stores` | 获取向量存储列表 |
|
||||
| GET | `/vector-stores/:id` | 获取向量存储详情 |
|
||||
| PUT | `/vector-stores/:id` | 更新向量存储(仅名称可改) |
|
||||
| DELETE | `/vector-stores/:id` | 删除向量存储(软删除) |
|
||||
| POST | `/vector-stores/:id/test` | 测试已保存或环境变量存储的连通性 |
|
||||
|
||||
## GET `/vector-stores/types` - 获取支持的引擎类型
|
||||
|
||||
返回所有支持的引擎类型及其连接配置字段、索引配置字段的定义,可用于前端动态表单生成。系统级元数据,无需鉴权感知,但仍需 `X-API-Key`。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/vector-stores/types' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"type": "elasticsearch",
|
||||
"display_name": "Elasticsearch (Keywords + Vector)",
|
||||
"connection_fields": [
|
||||
{ "name": "addr", "type": "string", "required": true, "description": "Elasticsearch URL (e.g., http://localhost:9200)" },
|
||||
{ "name": "username", "type": "string", "required": false },
|
||||
{ "name": "password", "type": "string", "required": false, "sensitive": true }
|
||||
],
|
||||
"index_fields": [
|
||||
{ "name": "index_name", "type": "string", "required": false, "default": "xwrag_default" },
|
||||
{ "name": "number_of_shards", "type": "number", "required": false },
|
||||
{ "name": "number_of_replicas", "type": "number", "required": false }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "postgres",
|
||||
"display_name": "PostgreSQL (Keywords + Vector)",
|
||||
"connection_fields": [
|
||||
{ "name": "use_default_connection", "type": "boolean", "required": false, "default": true, "description": "Use the application's default database connection" },
|
||||
{ "name": "addr", "type": "string", "required": false, "description": "PostgreSQL connection string (required if use_default_connection is false)" },
|
||||
{ "name": "username", "type": "string", "required": false },
|
||||
{ "name": "password", "type": "string", "required": false, "sensitive": true }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/vector-stores/test` - 使用原始凭据测试连接
|
||||
|
||||
用前端表单中尚未保存的凭据执行一次连通性测试,不会写入数据库。成功时返回自动检测到的服务器版本(如 ES 版本号);某些引擎(如 Milvus、SQLite)无法检测版本,`version` 会返回空字符串。
|
||||
|
||||
**参数说明(请求体)**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ------------------------------------------------------------- |
|
||||
| engine_type | string | 是 | 引擎类型,取自 `/vector-stores/types` 的 `type` |
|
||||
| connection_config | object | 是 | 该引擎对应的连接配置字段(与 `connection_fields` 对应) |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request POST 'http://localhost:8080/api/v1/vector-stores/test' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"engine_type": "elasticsearch",
|
||||
"connection_config": {
|
||||
"addr": "http://es:9200",
|
||||
"username": "elastic",
|
||||
"password": "changeme"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
**响应(成功)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"version": "7.10.1"
|
||||
}
|
||||
```
|
||||
|
||||
**响应(失败)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "failed to connect to elasticsearch: connection refused or authentication failed"
|
||||
}
|
||||
```
|
||||
|
||||
> 注意:测试失败时 HTTP 状态码仍为 `200`,错误信息通过 `success: false` + `error` 字段返回。
|
||||
|
||||
## POST `/vector-stores` - 创建向量存储
|
||||
|
||||
为当前空间创建一个新的向量存储配置。同一 endpoint + index 组合在空间内不允许重复(与环境变量配置的存储也会冲突)。
|
||||
|
||||
**参数说明(请求体)**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | --------------------------------------------------------------- |
|
||||
| name | string | 是 | 存储显示名(空间内友好名) |
|
||||
| engine_type | string | 是 | 引擎类型,取自 `/vector-stores/types` |
|
||||
| connection_config | object | 是 | 连接配置(与所选引擎的 `connection_fields` 对应) |
|
||||
| index_config | object | 否 | 索引配置(与所选引擎的 `index_fields` 对应) |
|
||||
|
||||
> Tencent VectorDB 使用 `engine_type: "tencent_vectordb"`。`connection_config` 中 `addr`、`username`、`api_key` 必填,`database` 可选;`index_config.collection_name` 表示集合名前缀,实际集合会按向量维度追加后缀(例如 `weknora_embeddings_768`);`index_config.replica_number` 表示创建集合时使用的副本数。该适配器同时支持向量检索和基于 BM25 sparse vector 的关键词检索;旧版本已创建且没有 `sparse_vector` 索引的集合需要重建并重新导入数据后才能启用关键词检索。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/vector-stores' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name": "elasticsearch-hot",
|
||||
"engine_type": "elasticsearch",
|
||||
"connection_config": {
|
||||
"addr": "http://es-hot:9200",
|
||||
"username": "elastic",
|
||||
"password": "changeme"
|
||||
},
|
||||
"index_config": {
|
||||
"index_name": "my_index"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
**Tencent VectorDB 请求示例**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/vector-stores' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name": "tencent-vectordb",
|
||||
"engine_type": "tencent_vectordb",
|
||||
"connection_config": {
|
||||
"addr": "http://your-instance.tencentvectordb.com",
|
||||
"username": "root",
|
||||
"api_key": "your_api_key",
|
||||
"database": "weknora"
|
||||
},
|
||||
"index_config": {
|
||||
"collection_name": "weknora_embeddings",
|
||||
"replica_number": 1
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
**响应** (201):
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "elasticsearch-hot",
|
||||
"engine_type": "elasticsearch",
|
||||
"connection_config": {
|
||||
"addr": "http://es-hot:9200",
|
||||
"username": "elastic",
|
||||
"password": "***"
|
||||
},
|
||||
"index_config": {
|
||||
"index_name": "my_index"
|
||||
},
|
||||
"source": "user",
|
||||
"readonly": false,
|
||||
"created_at": "2026-04-07T10:00:00Z",
|
||||
"updated_at": "2026-04-07T10:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 响应中的敏感字段(`password`、`api_key` 等)会被掩码为 `"***"`。`connection_config.version` 字段在连接测试成功后才会自动填充,创建时为空。
|
||||
|
||||
## GET `/vector-stores` - 获取向量存储列表
|
||||
|
||||
返回当前空间的所有向量存储,包含 `RETRIEVE_DRIVER` 环境变量配置的虚拟存储(`source: "env"`、`readonly: true`)和用户在 DB 中创建的存储(`source: "user"`、`readonly: false`)。环境变量存储排列在前。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/vector-stores' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": "__env_postgres__",
|
||||
"name": "postgres (env)",
|
||||
"engine_type": "postgres",
|
||||
"connection_config": {
|
||||
"use_default_connection": true
|
||||
},
|
||||
"source": "env",
|
||||
"readonly": true
|
||||
},
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "elasticsearch-hot",
|
||||
"engine_type": "elasticsearch",
|
||||
"connection_config": {
|
||||
"addr": "http://es-hot:9200",
|
||||
"username": "elastic",
|
||||
"password": "***"
|
||||
},
|
||||
"source": "user",
|
||||
"readonly": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## GET `/vector-stores/:id` - 获取向量存储详情
|
||||
|
||||
根据 ID 获取单个向量存储。支持 DB 存储 UUID 和 `__env_*` 形式的环境变量存储 ID(例如 `__env_postgres__`)。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ---- | ------ | ---- | --------------------------------------------------- |
|
||||
| id | string | 是 | 向量存储 ID(DB UUID 或 `__env_{driver}__`) |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/vector-stores/550e8400-e29b-41d4-a716-446655440000' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "elasticsearch-hot",
|
||||
"engine_type": "elasticsearch",
|
||||
"connection_config": {
|
||||
"addr": "http://es-hot:9200",
|
||||
"username": "elastic",
|
||||
"password": "***",
|
||||
"version": "7.10.1"
|
||||
},
|
||||
"index_config": {
|
||||
"index_name": "my_index"
|
||||
},
|
||||
"source": "user",
|
||||
"readonly": false,
|
||||
"created_at": "2026-04-07T10:00:00Z",
|
||||
"updated_at": "2026-04-07T10:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## PUT `/vector-stores/:id` - 更新向量存储
|
||||
|
||||
仅支持更新 `name`。`engine_type`、`connection_config`、`index_config` 创建后不可变更;环境变量存储不可修改(返回 `400`)。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ---- | ------ | ---- | -------------- |
|
||||
| id | string | 是 | 向量存储 ID |
|
||||
|
||||
**参数说明(请求体)**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ---- | ------ | ---- | ----------------- |
|
||||
| name | string | 是 | 新的存储显示名 |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request PUT 'http://localhost:8080/api/v1/vector-stores/550e8400-e29b-41d4-a716-446655440000' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name": "elasticsearch-hot-renamed"
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "elasticsearch-hot-renamed",
|
||||
"engine_type": "elasticsearch",
|
||||
"connection_config": {
|
||||
"addr": "http://es-hot:9200",
|
||||
"username": "elastic",
|
||||
"password": "***"
|
||||
},
|
||||
"index_config": {
|
||||
"index_name": "my_index"
|
||||
},
|
||||
"source": "user",
|
||||
"readonly": false,
|
||||
"created_at": "2026-04-07T10:00:00Z",
|
||||
"updated_at": "2026-04-07T10:05:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## DELETE `/vector-stores/:id` - 删除向量存储
|
||||
|
||||
对 DB 中的存储执行软删除。环境变量存储不可删除(返回 `400`)。
|
||||
|
||||
**Phase 2 — 绑定保护**:
|
||||
|
||||
删除请求在事务中执行,并按 `(tenant_id, vector_store_id)` 复合索引统计当前空间中仍绑定到该存储的活跃知识库数量。**只要存在任意绑定的知识库(已软删除的 KB 不计入),删除即被拒绝**,调用者必须先解绑或删除这些知识库才能继续。在 PostgreSQL 上,事务期间会对 `vector_stores` 行加 `SELECT … FOR UPDATE` 行锁,阻止并发的知识库创建请求悄悄落到正在被删除的存储上(SQLite 上则依赖 WAL + 单写入序列化达成同样语义)。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ---- | ------ | ---- | -------------- |
|
||||
| id | string | 是 | 向量存储 ID |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request DELETE 'http://localhost:8080/api/v1/vector-stores/550e8400-e29b-41d4-a716-446655440000' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应(成功)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
**响应(绑定保护触发)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": 1000,
|
||||
"message": "vector store still has 3 knowledge base(s) bound to it; unbind or delete them before removing the store"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
HTTP `400`。错误消息中包含具体的知识库数量(便于运营定位),但不包含任何 KB 的 ID/名称,以避免跨空间信息泄漏。删除被拒绝时,DB 中的存储行保持原状,进程内引擎注册表也不会被清除。
|
||||
|
||||
## POST `/vector-stores/:id/test` - 测试已保存或环境变量存储的连接
|
||||
|
||||
对已保存的 DB 存储或环境变量虚拟存储执行一次连接测试。成功时返回检测到的服务器版本;对 DB 存储,检测到的版本会被自动写回 `connection_config.version`,环境变量存储不会更新。
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ---- | ------ | ---- | --------------------------------------------------- |
|
||||
| id | string | 是 | 向量存储 ID(DB UUID 或 `__env_{driver}__`) |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request POST 'http://localhost:8080/api/v1/vector-stores/550e8400-e29b-41d4-a716-446655440000/test' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应(成功)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"version": "7.10.1"
|
||||
}
|
||||
```
|
||||
|
||||
**响应(失败)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "failed to connect to elasticsearch: connection refused or authentication failed"
|
||||
}
|
||||
```
|
||||
|
||||
> 与 `/vector-stores/test` 一致,测试失败时 HTTP 状态码仍为 `200`,错误通过 `success: false` + `error` 返回。
|
||||
|
||||
## 环境变量存储
|
||||
|
||||
通过 `RETRIEVE_DRIVER` 环境变量配置的向量存储以虚拟条目形式出现在列表和详情中。这些条目的特征:
|
||||
|
||||
- **ID 格式**:`__env_{driver}__`(如 `__env_postgres__`、`__env_elasticsearch_v8__`)
|
||||
- **source**:`"env"`
|
||||
- **readonly**:`true`
|
||||
- **不可修改/删除**:`PUT` 和 `DELETE` 返回 `400`
|
||||
- **可测试连通性**:`POST /vector-stores/:id/test` 正常工作
|
||||
- **被知识库绑定时**:未指定 `vector_store_id` 创建的知识库默认使用环境变量存储;这种知识库在响应中显示为 `vector_store_name="System default"` + `vector_store_source="env"`。
|
||||
|
||||
Tencent VectorDB 环境变量存储可通过 `TENCENT_VECTORDB_REPLICA_NUMBER` 覆盖默认集合副本数。默认值为 `1`;单节点 QA 环境可设为 `0`,生产环境可按 Tencent VectorDB 集群规模调整。
|
||||
|
||||
## 错误码
|
||||
|
||||
| HTTP 状态码 | code | 含义 |
|
||||
| ----------- | ---- | --------------------------------------------------- |
|
||||
| 400 | 1000 | 请求参数错误、校验失败、尝试修改环境变量存储、删除时仍有知识库绑定 |
|
||||
| 400 | 2200 | 知识库创建时引用的 `vector_store_id` 无效(不存在或属于其他空间) |
|
||||
| 400 | 2201 | 知识库创建时引用的存储当前不可用(DB 中存在但未注册到引擎) |
|
||||
| 401 | 1001 | 未认证(缺少空间上下文或 API Key) |
|
||||
| 404 | 1003 | 向量存储不存在 |
|
||||
| 409 | 1005 | 同一 endpoint + index 组合已存在 |
|
||||
| 500 | 1007 | 内部服务器错误 |
|
||||
|
||||
> `2200` / `2201` 由 `POST /knowledge-bases` 等知识库创建路径返回(详见 [knowledge-base.md](./knowledge-base.md)),列于此处仅为完整覆盖与向量存储相关的所有错误码。
|
||||
@@ -0,0 +1,278 @@
|
||||
# Web Search API
|
||||
|
||||
[返回目录](./README.md)
|
||||
|
||||
包含两组接口:
|
||||
- `/web-search/providers`:返回**可用的 provider 类型**(只读元数据)
|
||||
- `/web-search-providers/*`:当前空间**自定义保存**的 provider CRUD 与连通性测试
|
||||
|
||||
| 方法 | 路径 | 描述 |
|
||||
| ------ | ------------------------------------- | ----------------------------------- |
|
||||
| GET | `/web-search/providers` | 获取网络搜索服务商类型列表 |
|
||||
| GET | `/web-search-providers/types` | 获取 Provider 类型元数据(含参数定义) |
|
||||
| POST | `/web-search-providers/test` | 使用原始凭证测试连通性(不落库) |
|
||||
| POST | `/web-search-providers` | 创建空间级 Provider 配置 |
|
||||
| GET | `/web-search-providers` | 获取当前空间已保存的 Provider 列表 |
|
||||
| GET | `/web-search-providers/:id` | 获取指定 Provider 详情 |
|
||||
| PUT | `/web-search-providers/:id` | 更新 Provider |
|
||||
| DELETE | `/web-search-providers/:id` | 删除 Provider |
|
||||
| POST | `/web-search-providers/:id/test` | 使用已保存凭证测试连通性 |
|
||||
|
||||
## GET `/web-search/providers` - 获取网络搜索服务商类型列表
|
||||
|
||||
获取系统中可用的网络搜索服务商列表(系统级元数据,与空间无关)。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/web-search/providers' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"name": "google",
|
||||
"label": "Google Search",
|
||||
"description": "通过 Google 自定义搜索 API 进行网络搜索",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "bing",
|
||||
"label": "Bing Search",
|
||||
"description": "通过 Bing Search API 进行网络搜索",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "serpapi",
|
||||
"label": "SerpAPI",
|
||||
"description": "通过 SerpAPI 进行搜索引擎结果抓取",
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## GET `/web-search-providers/types` - 获取 Provider 类型元数据
|
||||
|
||||
返回 UI 表单需要的所有 provider 类型及参数定义(每种 provider 需要哪些字段、类型、是否必填)。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/web-search-providers/types' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"provider": "google",
|
||||
"label": "Google Search",
|
||||
"description": "...",
|
||||
"parameter_schema": [
|
||||
{ "name": "api_key", "label": "API Key", "type": "string", "required": true },
|
||||
{ "name": "cx", "label": "Search Engine ID", "type": "string", "required": true }
|
||||
]
|
||||
}
|
||||
],
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## POST `/web-search-providers/test` - 使用原始凭证测试连通性
|
||||
|
||||
前端表单"测试连接"按钮使用:用尚未保存的凭证发起一次样本搜索。
|
||||
|
||||
**参数说明(请求体)**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ---------- | ------ | ---- | --------------------------------- |
|
||||
| provider | string | 是 | provider 类型(如 `google`、`bing`) |
|
||||
| parameters | object | 是 | 该 provider 所需凭证与参数(与 `/types` 中 `parameter_schema` 对应) |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request POST 'http://localhost:8080/api/v1/web-search-providers/test' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"provider": "google",
|
||||
"parameters": {
|
||||
"api_key": "AIza...",
|
||||
"cx": "0123456789:abcdefg"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{ "success": true }
|
||||
```
|
||||
|
||||
失败时:
|
||||
|
||||
```json
|
||||
{ "success": false, "error": "google api: 403 forbidden" }
|
||||
```
|
||||
|
||||
### 智谱 AI 配置
|
||||
|
||||
智谱使用独立的 Web Search API。`search_engine` 和 `content_size` 存放在
|
||||
`parameters.extra_config` 中;未指定时分别使用 `search_std` 和 `medium`。
|
||||
|
||||
```json
|
||||
{
|
||||
"provider": "zhipu",
|
||||
"parameters": {
|
||||
"api_key": "your-zhipu-api-key",
|
||||
"extra_config": {
|
||||
"search_engine": "search_std",
|
||||
"content_size": "medium"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`search_engine` 支持 `search_std`、`search_pro`、`search_pro_sogou` 和
|
||||
`search_pro_quark`;`content_size` 支持 `medium` 和 `high`。
|
||||
|
||||
## POST `/web-search-providers` - 创建 Provider
|
||||
|
||||
**参数说明(请求体)**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| ----------- | ------- | ---- | ------------------------------------------ |
|
||||
| name | string | 是 | Provider 显示名(在空间内唯一友好名) |
|
||||
| provider | string | 是 | Provider 类型(来自 `/web-search-providers/types`) |
|
||||
| description | string | 否 | 备注 |
|
||||
| parameters | object | 否 | 凭证与参数 |
|
||||
| is_default | boolean | 否 | 是否设为当前空间默认 Provider |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/web-search-providers' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name": "公司 Google CSE",
|
||||
"provider": "google",
|
||||
"description": "用于内网搜索",
|
||||
"parameters": {
|
||||
"api_key": "AIza...",
|
||||
"cx": "0123456789:abcdefg"
|
||||
},
|
||||
"is_default": true
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "wsp-...",
|
||||
"tenant_id": 1,
|
||||
"name": "公司 Google CSE",
|
||||
"provider": "google",
|
||||
"is_default": true,
|
||||
"parameters": { "api_key": "***", "cx": "0123456789:abcdefg" }
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## GET `/web-search-providers` - 获取 Provider 列表
|
||||
|
||||
返回当前空间已保存的所有 Provider。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/web-search-providers' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{ "id": "wsp-001", "name": "公司 Google CSE", "provider": "google", "is_default": true }
|
||||
],
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## GET `/web-search-providers/:id` - 获取 Provider 详情
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ---- | ------ | ----------- |
|
||||
| id | string | Provider ID |
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location 'http://localhost:8080/api/v1/web-search-providers/wsp-001' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应**: 同创建接口。404 表示不存在。
|
||||
|
||||
## PUT `/web-search-providers/:id` - 更新 Provider
|
||||
|
||||
**说明**:`provider` 字段(类型)创建后不可修改,仅支持更新 `name` / `description` / `parameters` / `is_default`。
|
||||
|
||||
**参数说明(请求体)**: 同创建接口,但不包含 `provider`。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request PUT 'http://localhost:8080/api/v1/web-search-providers/wsp-001' \
|
||||
--header 'X-API-Key: sk-xxxxx' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name": "公司 Google CSE (v2)",
|
||||
"parameters": { "api_key": "NEW...", "cx": "0123456789:abcdefg" },
|
||||
"is_default": false
|
||||
}'
|
||||
```
|
||||
|
||||
**响应**: `{ "data": {...更新后实体...}, "success": true }`
|
||||
|
||||
## DELETE `/web-search-providers/:id` - 删除 Provider
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request DELETE 'http://localhost:8080/api/v1/web-search-providers/wsp-001' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应**: `{ "success": true }`
|
||||
|
||||
## POST `/web-search-providers/:id/test` - 测试已保存的 Provider
|
||||
|
||||
使用数据库中已保存的凭证发起一次样本搜索。
|
||||
|
||||
**请求**:
|
||||
|
||||
```curl
|
||||
curl --location --request POST 'http://localhost:8080/api/v1/web-search-providers/wsp-001/test' \
|
||||
--header 'X-API-Key: sk-xxxxx'
|
||||
```
|
||||
|
||||
**响应**: 同 `POST /web-search-providers/test`。
|
||||
@@ -0,0 +1,22 @@
|
||||
# WeKnora 云镜像打包指南
|
||||
|
||||
把 WeKnora 打包成可分发的云镜像(AMI / 自定义镜像 / Snapshot),用户基于镜像创建实例后开机即用、自动随机化密钥、零私密泄漏。
|
||||
|
||||
## 通用工具
|
||||
|
||||
云无关的脚本和详细说明:[`scripts/cloud-image/README.md`](../../scripts/cloud-image/README.md)
|
||||
|
||||
包含 `prepare.sh` / `cleanup.sh` / `firstboot.sh` 三个脚本和两个 systemd 单元,已在多种发行版(Ubuntu / Debian / CentOS / TencentOS)上验证。
|
||||
|
||||
## 各平台具体操作
|
||||
|
||||
| 平台 | 文档 | 状态 |
|
||||
|---|---|---|
|
||||
| 腾讯云轻量应用服务器 / CVM | [tencent-lighthouse.md](./tencent-lighthouse.md) | ✅ |
|
||||
| AWS EC2 (AMI) | _欢迎贡献_ | ⏳ |
|
||||
| 阿里云 ECS | _欢迎贡献_ | ⏳ |
|
||||
| 火山引擎 ECS | _欢迎贡献_ | ⏳ |
|
||||
| 华为云 ECS | _欢迎贡献_ | ⏳ |
|
||||
| 本地 KVM / Proxmox | _欢迎贡献_ | ⏳ |
|
||||
|
||||
> 各平台文档结构尽量保持一致:实例规格建议 → 制作镜像操作 → 共享/公开方式 → 该平台独有的注意事项。
|
||||
@@ -0,0 +1,100 @@
|
||||
# 腾讯云轻量应用服务器 / CVM 镜像制作指南
|
||||
|
||||
> **前置阅读**:通用脚本与流程见 [`scripts/cloud-image/README.md`](../../scripts/cloud-image/README.md)。本文只补充腾讯云平台专属的操作步骤。
|
||||
|
||||
## 实例规格建议
|
||||
|
||||
| 项 | 建议值 |
|
||||
|---|---|
|
||||
| 实例类型 | 轻量应用服务器(Lighthouse) / 云服务器 CVM 标准型 |
|
||||
| CPU / 内存 | 至少 4 核 8G(推荐 4 核 16G,跑全功能 RAG + Agent) |
|
||||
| 系统盘 | 至少 80G SSD |
|
||||
| 镜像 | Ubuntu Server 22.04 LTS / TencentOS Server 3.1 |
|
||||
| 地域 | 选你后续主要用户所在地域(同地域才能跨账号共享) |
|
||||
|
||||
## 完整流程
|
||||
|
||||
1. 控制台买一台符合上述规格的实例
|
||||
2. SSH 进去,按 [scripts/cloud-image/README.md](../../scripts/cloud-image/README.md) 执行 `prepare.sh`
|
||||
3. 浏览器访问 `http://<公网IP>` 验证功能
|
||||
4. 执行 `cleanup.sh`(会自动 `poweroff`)
|
||||
5. 控制台「**制作镜像**」(见下)
|
||||
6. 用新镜像创建一台测试实例,验证 firstboot 工作正常
|
||||
7. 「**共享镜像**」给其他账号 / 申请「**镜像市场**」上架(见下)
|
||||
|
||||
## 制作镜像
|
||||
|
||||
**轻量应用服务器**:
|
||||
|
||||
1. 控制台 → 轻量应用服务器 → 选中已关机的实例
|
||||
2. 「更多」→「**制作镜像**」
|
||||
3. 镜像名建议带版本号:`weknora-v0.5.0-ubuntu2204`
|
||||
4. 等待 5–30 分钟(取决于系统盘大小)
|
||||
|
||||
**云服务器 CVM**:
|
||||
|
||||
1. 控制台 → 云服务器 → 选中已关机的实例
|
||||
2. 「更多」→「制作镜像」→ 选「整机镜像」
|
||||
3. 同样建议带版本号
|
||||
|
||||
> 同一账号下,自定义镜像有数量配额(默认 20 个),可在控制台查看。
|
||||
|
||||
## 验证镜像
|
||||
|
||||
强烈建议用新镜像创建一台测试实例,至少验证:
|
||||
|
||||
- [ ] 能 SSH 进去(用控制台的默认密码 / 你导入的 key)
|
||||
- [ ] `systemctl status weknora-firstboot` 显示已成功执行(或已 disable + 文件被删)
|
||||
- [ ] `cat /root/weknora-credentials.txt` 里有随机密码
|
||||
- [ ] 浏览器打开公网 IP 能访问 WeKnora,能注册管理员
|
||||
- [ ] `docker compose -f /opt/WeKnora/docker-compose.yml ps` 全部 healthy
|
||||
- [ ] `cat /opt/WeKnora/.cloud-image-meta` 显示正确的版本
|
||||
|
||||
## 共享给其他用户
|
||||
|
||||
按「覆盖范围」递增有 3 种方式:
|
||||
|
||||
### 方式 A:跨账号共享(私下分享)
|
||||
|
||||
控制台 → 自定义镜像 → 选中镜像 → 「**共享**」→ 输入对方腾讯云账号 ID(UIN)。
|
||||
|
||||
- 对方在自己的「共享镜像」列表能看到,可直接基于它创建实例
|
||||
- **限制**:必须同地域;对方账号必须已开通对应产品(Lighthouse / CVM)
|
||||
- 适合小范围、合作伙伴、内测用户
|
||||
|
||||
### 方式 B:跨地域使用
|
||||
|
||||
Lighthouse 的镜像可以「共享给 CVM」,转成 CVM 自定义镜像后即可:
|
||||
|
||||
- 跨地域复制
|
||||
- 导出为 `qcow2` 文件下载到本地(更通用,可用于 KVM / 其他云)
|
||||
|
||||
### 方式 C:通过腾讯云市场上架镜像商品(公开一键部署)
|
||||
|
||||
这是真正「任何用户都能在云市场搜到并一键购买/部署」的形态。流程比较重,适合长期运营。
|
||||
|
||||
参考文档(请以官方为准):
|
||||
|
||||
- [云市场 - 镜像服务上架流程](https://cloud.tencent.com/document/product/306/3019)
|
||||
- [云市场 - 镜像商品制作说明](https://cloud.tencent.com/document/product/306/30128)
|
||||
- [Lighthouse - 应用镜像使用说明](https://cloud.tencent.com/document/product/1207/72665)
|
||||
- [Lighthouse - 管理镜像(操作指南)](https://cloud.tencent.com/document/product/1207/63263)
|
||||
|
||||
大致步骤:
|
||||
|
||||
1. 登录 [腾讯云市场服务商管理控制台](https://console.cloud.tencent.com/serviceprovider),注册成为服务商
|
||||
2. 「商品管理 → 商品列表 → 新建商品」,接入类型选「**镜像**」
|
||||
3. 选择已制作好的自定义镜像(即上面方式 A/B 制作的那份)
|
||||
4. **完成主机安全(专业版)检测**——这是镜像上架的硬性前置条件,需要自付 CVM + 主机安全专业版费用,建议用按量付费跑完即释放
|
||||
5. 填写商品名称、版本、亮点、详情、使用指南(必传 PDF/Word/PPT/ZIP/RAR,≤2MB)、售后说明
|
||||
6. 选择售卖方式(按量计费 / 按周期计费),按量计费目前仅支持 0 元规格
|
||||
7. 提交审核(云市场运营人员审核约 7 个工作日)
|
||||
8. 审核通过后,用户在云市场或购买 CVM 时即可选到你的镜像
|
||||
|
||||
> WeKnora 是腾讯系开源项目(`Tencent/WeKnora`),如果想推动官方上架,建议在 [WeKnora GitHub Issues](https://github.com/Tencent/WeKnora/issues) 联系维护团队,而不是个人单独申请。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 腾讯云的 cloud-init 兼容性良好,`cleanup.sh` 里的 `cloud-init clean` 能正常工作
|
||||
- Lighthouse 的镜像默认大小限制为系统盘大小,请提前预估
|
||||
- 若使用域名 + HTTPS,建议在 firstboot 之外通过 [acme.sh](https://acme.sh) / certbot 单独配置,不要把证书烤进镜像
|
||||
@@ -0,0 +1,123 @@
|
||||
# OpenSearch k-NN driver — local integration test
|
||||
|
||||
This guide brings up a single-node OpenSearch cluster and exercises the
|
||||
OpenSearch retrieve engine end to end. The driver lives in
|
||||
`internal/application/repository/retriever/opensearch/`.
|
||||
|
||||
## 1. Start a dev cluster
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.dev.yml --profile opensearch up -d
|
||||
```
|
||||
|
||||
This starts:
|
||||
|
||||
- `opensearch` on `http://localhost:9200` — single-node, **security plugin
|
||||
disabled** (plain HTTP, no auth/TLS). The image bundles the
|
||||
`opensearch-knn` plugin.
|
||||
|
||||
> **OpenSearch Dashboards is optional** and lives in a separate
|
||||
> `opensearch-ui` profile, so it is *not* started by `--profile opensearch`.
|
||||
> The whole integration test below is curl-verifiable against `:9200`. If you
|
||||
> want the web UI (Dev Tools console / visual index inspection), start it on
|
||||
> demand:
|
||||
>
|
||||
> ```bash
|
||||
> docker compose -f docker-compose.dev.yml --profile opensearch-ui up -d
|
||||
> # opensearch-dashboards on http://localhost:5601 (depends_on pulls the cluster in)
|
||||
> ```
|
||||
|
||||
Verify:
|
||||
|
||||
```bash
|
||||
curl -s localhost:9200 | jq '.version.distribution, .version.number'
|
||||
# "opensearch" "3.3.2"
|
||||
curl -s 'localhost:9200/_cat/plugins?format=json' | jq -r '.[].component' | grep opensearch-knn
|
||||
```
|
||||
|
||||
> Production clusters must enable the security plugin (TLS + auth). The dev
|
||||
> profile disables it only to keep local setup trivial. When connecting to a
|
||||
> secured cluster, set `username` / `password` and — for self-signed certs in
|
||||
> dev only — `insecure_skip_verify=true`.
|
||||
|
||||
## 2. Register the store
|
||||
|
||||
### Option A — DB store (UI / API)
|
||||
|
||||
> **SSRF whitelist (dev).** `CreateStore` and the raw connection test validate
|
||||
> the user-supplied `addr` against the SSRF policy. `http://localhost:9200`
|
||||
> is rejected by default — `localhost` is a restricted hostname and `9200` is
|
||||
> a blocked port. When the backend runs on the host (`go run`), add `localhost`
|
||||
> to the whitelist in your `.env` before registering:
|
||||
>
|
||||
> ```bash
|
||||
> SSRF_WHITELIST=localhost
|
||||
> ```
|
||||
>
|
||||
> The containerised compose deployment whitelists the bundled vector-store
|
||||
> service names automatically (`SSRF_WHITELIST_EXTRA`), so this step is
|
||||
> dev-only. The env-store path (Option B) is not affected.
|
||||
|
||||
`POST /api/v1/vector-stores`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "opensearch-local",
|
||||
"engine_type": "opensearch",
|
||||
"connection_config": { "addr": "http://localhost:9200" },
|
||||
"index_config": {
|
||||
"number_of_shards": 1,
|
||||
"number_of_replicas": 0,
|
||||
"hnsw_m": 16,
|
||||
"hnsw_ef_construction": 100,
|
||||
"knn_engine": "lucene"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`CreateStore` runs the connection probe (version + k-NN plugin) before
|
||||
persisting; a bad address / unsupported version / missing plugin is rejected
|
||||
with `400`.
|
||||
|
||||
### Option B — env store
|
||||
|
||||
```bash
|
||||
export RETRIEVE_DRIVER=opensearch
|
||||
export OPENSEARCH_ADDR=http://localhost:9200
|
||||
# export OPENSEARCH_USERNAME / OPENSEARCH_PASSWORD for a secured cluster
|
||||
# export OPENSEARCH_INSECURE_SKIP_VERIFY=true # self-signed dev TLS only
|
||||
```
|
||||
|
||||
## 3. Single-node note (important)
|
||||
|
||||
On a single-node cluster, any index created with `number_of_replicas >= 1`
|
||||
leaves its replica shard **unassigned**, so the index health goes **Yellow**.
|
||||
Yellow does **not** block reads or writes — it is safe for local testing — but
|
||||
to keep the cluster Green set **`number_of_replicas: 0`** at store
|
||||
registration (as in the Option A example above). The driver default is `1`
|
||||
(it assumes a ≥2-node cluster).
|
||||
|
||||
## 4. Exercise the flow
|
||||
|
||||
1. Bind a knowledge base to the store and ingest a few documents.
|
||||
2. Confirm the per-dimension index appears:
|
||||
`curl -s 'localhost:9200/_cat/indices?v' | grep weknora`
|
||||
(e.g. `weknora_<storeprefix>_768` + alias, plus `weknora_<storeprefix>_keywords`).
|
||||
3. Run a retrieval query against the bound KB and confirm hits come back.
|
||||
4. Copy the KB to another KB and confirm the docs are reindexed
|
||||
(`opensearch.reindex_executed` audit event).
|
||||
5. Toggle chunk enabled-status / tag and confirm `_update_by_query` applies it.
|
||||
|
||||
## 5. Tear down
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.dev.yml --profile opensearch down -v
|
||||
```
|
||||
|
||||
## Scope notes
|
||||
|
||||
- Large-batch async reindex / delete (task polling) is a follow-up; the sync
|
||||
paths handle typical KB sizes (pagination is bounded by `max_result_window`,
|
||||
default 10000).
|
||||
- Native `hybrid` query + search pipeline is out of scope — fusion stays at the
|
||||
service layer (RRF).
|
||||
@@ -0,0 +1,182 @@
|
||||
# Embed 安全模式
|
||||
|
||||
> **一句话**:长期密钥(发布 Token `em_…`)只放在**你自己的服务器**;访客浏览器里只有 30 分钟有效的短时令牌(`ems_…`)。
|
||||
|
||||
## 为什么要用?
|
||||
|
||||
| 方式 | 发布 Token 在哪 | 风险 |
|
||||
|------|-----------------|------|
|
||||
| iframe / 普通 Widget | 写在页面 HTML 或 URL hash 里 | 任何人「查看源代码」就能复制,等于公开密钥 |
|
||||
| **安全模式 Widget** | 仅环境变量 / 密钥管理,在服务端 | 浏览器拿不到长期密钥;还可先校验访客是否登录 |
|
||||
|
||||
生产环境对外嵌入,**应优先用安全模式**。
|
||||
|
||||
## 两种 Token
|
||||
|
||||
| 名称 | 格式 | 谁持有 | 用途 |
|
||||
|------|------|--------|------|
|
||||
| 发布 Token | `em_…` | 仅你的服务端 | 向 WeKnora 换取短时令牌;在管理端「渠道密钥」查看 |
|
||||
| 会话 Token | `ems_…` | 访客浏览器(iframe 内) | 调聊天、上传等 embed API;约 30 分钟过期,Widget 会自动刷新 |
|
||||
|
||||
## 工作流程
|
||||
|
||||
```
|
||||
访客浏览器 你的后端(shop 的服务器) WeKnora
|
||||
│ │ │
|
||||
│ 1. 加载 Widget │ │
|
||||
│ data-token-endpoint │ │
|
||||
│ (不含 em_) │ │
|
||||
│─────────────────────────────►│ │
|
||||
│ │ 2. 校验访客已登录(可选) │
|
||||
│ │ 3. POST .../embed/:id/exchange │
|
||||
│ │ Authorization: Embed em_… │
|
||||
│ │─────────────────────────────►│
|
||||
│ │◄──── session_token (ems_…) ───│
|
||||
│◄── 4. { token, expiresIn } ──│ │
|
||||
│ 5. iframe 用 ems_ 聊天 │ │
|
||||
```
|
||||
|
||||
对应管理端「嵌入渠道 → 安全模式」里的两段代码:
|
||||
|
||||
1. **页面脚本**:`data-token-endpoint="https://你的域名/weknora/embed-token"`(没有 `data-token`)
|
||||
2. **服务端接口**:用发布 Token 调 exchange,把 `ems_…` 返回给前端
|
||||
|
||||
## 集成步骤
|
||||
|
||||
### 第 1 步:在 WeKnora 创建渠道
|
||||
|
||||
- 记下 **渠道 ID** 和 **发布 Token**(`em_…`)
|
||||
- 配置域名白名单(见下)
|
||||
- 配置分钟 / 日限流
|
||||
|
||||
### 第 2 步:部署取令牌接口
|
||||
|
||||
在你的业务后端新增一个 HTTP 接口(路径自定),要求:
|
||||
|
||||
**入参**:浏览器 GET 请求(Widget 会 `fetch` 这个地址)
|
||||
|
||||
**你必须做**:
|
||||
|
||||
- 校验调用方是合法访客(Session Cookie、JWT 等),未登录返回 `401`
|
||||
- 用发布 Token 调 WeKnora exchange
|
||||
- 成功时返回 JSON:`{ "token": "<ems_…>", "expiresIn": 1800 }`
|
||||
|
||||
**调 exchange 的约定**:
|
||||
|
||||
```http
|
||||
POST https://<weknora-host>/api/v1/embed/<channel_id>/exchange
|
||||
Authorization: Embed <发布 Token em_…>
|
||||
Origin: https://<你的业务站点> ← 须与渠道白名单一致,否则 403
|
||||
```
|
||||
|
||||
> 服务端 `fetch` 默认不带 `Origin`,需要**手动设置**与白名单匹配的 `Origin` 头。
|
||||
|
||||
### 第 3 步:粘贴安全模式 Widget 代码
|
||||
|
||||
把 `data-token-endpoint` 改成上一步的真实 URL。完整示例在管理端「安全模式」Tab 可复制。
|
||||
|
||||
## 服务端示例
|
||||
|
||||
以下 `<WEKNORA_HOST>`、`<CHANNEL_ID>` 替换为实际值;发布 Token 放环境变量 `WEKNORA_PUBLISH_TOKEN`,**不要**写进前端。
|
||||
|
||||
### Node.js(Express)
|
||||
|
||||
```javascript
|
||||
const WEKNORA_BASE = 'https://<WEKNORA_HOST>';
|
||||
const CHANNEL_ID = '<CHANNEL_ID>';
|
||||
const ALLOWED_ORIGIN = 'https://shop.example.com'; // 与渠道白名单一致
|
||||
|
||||
app.get('/weknora/embed-token', async (req, res) => {
|
||||
const hasSession = Boolean(req.cookies?.session_id);
|
||||
const auth = req.headers.authorization || '';
|
||||
if (!hasSession && !auth.startsWith('Bearer ')) {
|
||||
return res.status(401).json({ error: 'unauthorized' });
|
||||
}
|
||||
|
||||
const r = await fetch(`${WEKNORA_BASE}/api/v1/embed/${CHANNEL_ID}/exchange`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Embed ' + process.env.WEKNORA_PUBLISH_TOKEN,
|
||||
Origin: ALLOWED_ORIGIN,
|
||||
},
|
||||
});
|
||||
const body = await r.json();
|
||||
if (!body?.data?.session_token) {
|
||||
return res.status(502).json({ error: 'mint failed' });
|
||||
}
|
||||
res.json({ token: body.data.session_token, expiresIn: body.data.expires_in });
|
||||
});
|
||||
```
|
||||
|
||||
### Go(net/http)
|
||||
|
||||
```go
|
||||
func embedTokenHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Authorization") == "" && r.Header.Get("Cookie") == "" {
|
||||
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
req, _ := http.NewRequest(http.MethodPost,
|
||||
"https://<WEKNORA_HOST>/api/v1/embed/<CHANNEL_ID>/exchange", nil)
|
||||
req.Header.Set("Authorization", "Embed "+os.Getenv("WEKNORA_PUBLISH_TOKEN"))
|
||||
req.Header.Set("Origin", "https://shop.example.com") // 与渠道白名单一致
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil || resp.StatusCode >= 300 {
|
||||
http.Error(w, `{"error":"mint failed"}`, http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var body struct {
|
||||
Data struct {
|
||||
SessionToken string `json:"session_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if json.NewDecoder(resp.Body).Decode(&body) != nil || body.Data.SessionToken == "" {
|
||||
http.Error(w, `{"error":"mint failed"}`, http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"token": body.Data.SessionToken, "expiresIn": body.Data.ExpiresIn,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
管理端「安全模式 → 服务端示例」Tab 会按当前渠道 ID 生成带真实 URL 的片段。
|
||||
|
||||
## 域名白名单怎么填?
|
||||
|
||||
| 要放行的请求来源 | 白名单示例 |
|
||||
|------------------|------------|
|
||||
| 聊天 iframe 所在源站(embed 页面) | `https://app.example.com` 或 `https://embed.example.com` |
|
||||
| 你的取令牌后端(exchange 时带的 Origin) | `https://shop.example.com` |
|
||||
|
||||
若 embed 使用[独立子域](./embed-subdomain.md),**两条都要加**(embed 源站 + 业务后端源站)。
|
||||
|
||||
开发环境可临时使用 `*`;**生产环境禁止 `*`**。
|
||||
|
||||
## 上线检查
|
||||
|
||||
- [ ] 发布 Token 仅通过环境变量 / 密钥服务注入,未提交到 Git、未打进前端静态包
|
||||
- [ ] 取令牌接口校验访客身份
|
||||
- [ ] 全链路 HTTPS
|
||||
- [ ] 白名单已包含 embed 源站与 exchange 使用的 Origin
|
||||
- [ ] 已配置限流;敏感智能体不要用普通模式把 Token 暴露在网页里
|
||||
- [ ] 轮换发布 Token 后,同步更新服务端环境变量
|
||||
|
||||
## 常见问题
|
||||
|
||||
| 现象 | 原因与处理 |
|
||||
|------|------------|
|
||||
| exchange 返回 **401** / `publish token required` | 发布 Token 错误、已轮换,或误用了 `ems_` 会话 Token |
|
||||
| exchange 或聊天 API 返回 **403** `origin not allowed` | 白名单未包含当前请求的 `Origin`;服务端 exchange 记得手动加 `Origin` 头 |
|
||||
| iframe 一直「等待 Token」 | `token-endpoint` 未返回 `{ token, expiresIn }`,或 CORS 未允许 Widget 所在源站访问你的接口 |
|
||||
| 取令牌接口 **502** `mint failed` | WeKnora 不可达、渠道已停用,或 exchange 响应格式不对 |
|
||||
| 访客随便就能聊 | 取令牌接口未做登录校验——在 exchange 前加 Session / JWT 检查 |
|
||||
|
||||
## 相关
|
||||
|
||||
- 可选:embed 独立子域 → [embed-subdomain.md](./embed-subdomain.md)
|
||||
- Widget SDK 注释:`frontend/public/weknora-widget.js`
|
||||
- 代码生成:`frontend/src/api/embed/index.ts`
|
||||
@@ -0,0 +1,106 @@
|
||||
# Embed 独立子域部署(可选)
|
||||
|
||||
> **一句话**:把聊天页面单独放到 `embed.example.com`,与主站 `app.example.com` 分开。**大多数部署不需要这一步**——主站和 embed 同域就能用。只有对安全隔离有明确要求时再考虑。
|
||||
|
||||
## 先搞清楚三个「网站」
|
||||
|
||||
嵌入聊天时,实际涉及三类地址(可以相同,也可以不同):
|
||||
|
||||
| 角色 | 举例 | 干什么 |
|
||||
|------|------|--------|
|
||||
| **A. 业务站点(宿主)** | `https://shop.example.com` | 你的商城 / 文档站;在这里粘贴 Widget 脚本或 iframe |
|
||||
| **B. Embed 页面源站** | `https://app.example.com` 或 `https://embed.example.com` | 提供 `embed.html`、`weknora-widget.js`;聊天 iframe 加载自这里 |
|
||||
| **C. WeKnora API** | 通常与 B 同域,如 `https://app.example.com/api` | 后端接口 |
|
||||
|
||||
**默认(推荐入门)**:B 和主站管理后台都在 `https://app.example.com`,A 可以是任意第三方域名。
|
||||
|
||||
```
|
||||
shop.example.com app.example.com
|
||||
┌─────────────────┐ ┌──────────────────────────┐
|
||||
│ <script src= │ │ /weknora-widget.js │
|
||||
│ app.../widget> │──────►│ /embed/:channelId │
|
||||
│ │ │ /api/v1/embed/... │
|
||||
│ (浮窗 iframe) │◄──────│ │
|
||||
└─────────────────┘ └──────────────────────────┘
|
||||
```
|
||||
|
||||
**独立子域(进阶)**:把 B 拆到 `https://embed.example.com`,主站 `https://app.example.com` 只留管理后台。
|
||||
|
||||
```
|
||||
shop.example.com embed.example.com app.example.com
|
||||
┌─────────────────┐ ┌──────────────────┐ ┌─────────────┐
|
||||
│ Widget 脚本 │──────►│ embed 静态页+API │ │ 管理后台 │
|
||||
│ iframe 指向 embed│◄──────│ (无 index SPA) │ │ (可选分离) │
|
||||
└─────────────────┘ └──────────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
## 什么时候需要独立子域?
|
||||
|
||||
| 场景 | 是否需要 |
|
||||
|------|----------|
|
||||
| 内测、PoC、单域 Docker 部署 | **不需要** |
|
||||
| 主站 `X-Frame-Options: SAMEORIGIN`,但要把聊天嵌到第三方页面 | **不需要**(被嵌的是 `/embed/*` 页面,不是管理后台 SPA) |
|
||||
| 希望 embed 站点不携带主站登录 Cookie、缩小暴露面 | **可以考虑** |
|
||||
| 希望 CDN / WAF 对 embed 流量单独限速、缓存 | **可以考虑** |
|
||||
| 合规要求「对外嵌入」与「内部管理」必须不同源 | **需要** |
|
||||
|
||||
本地开发:`http://localhost:5173` 同域即可,Vite 已把 `/embed/*` 指到 `embed.html`,**不必**配子域。
|
||||
|
||||
## 怎么配置
|
||||
|
||||
### 1. 告诉管理端「embed 页面公网地址」
|
||||
|
||||
管理端生成 iframe / Widget 代码时,需要知道 B 的地址。在 `frontend/public/config.js`(或 Docker 启动脚本生成的配置)里设置:
|
||||
|
||||
```javascript
|
||||
window.__RUNTIME_CONFIG__ = {
|
||||
// embed 页面与 widget.js 的源站;留空则使用当前浏览器所在域名
|
||||
EMBED_BASE_URL: 'https://embed.example.com',
|
||||
MAX_FILE_SIZE_MB: 50,
|
||||
};
|
||||
```
|
||||
|
||||
构建期也可通过环境变量注入:`VITE_EMBED_BASE_URL=https://embed.example.com`(可选)。
|
||||
|
||||
### 2. Nginx:单独 server 块只服务 embed
|
||||
|
||||
`frontend/nginx.conf` 顶部有注释掉的示例。要点:
|
||||
|
||||
- `server_name embed.example.com`
|
||||
- 只暴露:`/embed/*` → `embed.html`,`/weknora-widget.js`,`/assets/*`
|
||||
- `/api/` 反代到 WeKnora 后端(与主站相同后端即可)
|
||||
- **不要**在这个 server 上挂完整 `index.html` 管理 SPA(减少攻击面)
|
||||
|
||||
主站 `server` 块可继续 `X-Frame-Options: SAMEORIGIN`,不影响第三方嵌 embed 子域的 iframe。
|
||||
|
||||
### 3. 域名白名单填什么?
|
||||
|
||||
渠道里的「域名白名单」校验的是 **API 请求的 `Origin` 头**,不是「允许哪些网站粘贴脚本」的抽象概念。
|
||||
|
||||
实际要填:
|
||||
|
||||
1. **Embed 页面源站**(必填)——聊天 iframe 内发 API 时浏览器会带这个 Origin
|
||||
- 同域部署:`https://app.example.com`
|
||||
- 独立子域:`https://embed.example.com`
|
||||
2. **你的业务后端源站**(安全模式时建议填)——你自己写的「取令牌接口」在服务端调用 `POST .../exchange` 时,需带与白名单一致的 `Origin` 头(见 [embed-secure-mode.md](./embed-secure-mode.md))
|
||||
|
||||
第三方商城 `https://shop.example.com` **通常不用**进白名单(它只是加载脚本,不直接调 embed API)。
|
||||
若你把 embed 反代到商城同域路径下(少见),才需要填商城域名。
|
||||
|
||||
## Widget 跨域与 sandbox
|
||||
|
||||
当宿主站 A 与 embed 源站 B **不同域**时,`weknora-widget.js` 会自动给内部 iframe 加上 `sandbox`(也可手动 `data-sandbox="true"`)。
|
||||
|
||||
A 与 B 同域时保持默认即可,无需 `data-sandbox`。
|
||||
|
||||
## 检查清单
|
||||
|
||||
- [ ] `EMBED_BASE_URL` 与真实访问地址一致(含 `https`)
|
||||
- [ ] embed 子域能打开 `/embed/<渠道ID>` 和 `/weknora-widget.js`
|
||||
- [ ] embed 子域 `/api/` 能连到后端
|
||||
- [ ] 渠道白名单包含 embed 源站(及安全模式下的业务后端源站)
|
||||
- [ ] 管理端复制的 snippet 里 URL 已变为 embed 子域
|
||||
|
||||
## 相关文档
|
||||
|
||||
- 发布 Token 不落浏览器:[embed-secure-mode.md](./embed-secure-mode.md)
|
||||
|
After Width: | Height: | Size: 592 KiB |
|
After Width: | Height: | Size: 834 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 685 KiB |
|
After Width: | Height: | Size: 816 KiB |
|
After Width: | Height: | Size: 397 KiB |
|
After Width: | Height: | Size: 339 KiB |
|
After Width: | Height: | Size: 610 KiB |
|
After Width: | Height: | Size: 394 KiB |
|
After Width: | Height: | Size: 398 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 504 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 492 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 416 KiB |
|
After Width: | Height: | Size: 888 KiB |
|
After Width: | Height: | Size: 539 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 670 KiB |
@@ -0,0 +1,181 @@
|
||||
# Database migration troubleshooting
|
||||
|
||||
This guide is linked from the system info page when WeKnora's startup database
|
||||
migration fails. It covers the most common causes, how to diagnose them, and
|
||||
how to recover without losing data.
|
||||
|
||||
If none of these match your situation, jump to
|
||||
[Reporting an issue](#reporting-an-issue) at the bottom.
|
||||
|
||||
---
|
||||
|
||||
## What "migration failed" means
|
||||
|
||||
WeKnora auto-runs `golang-migrate` migrations on every startup. When a
|
||||
migration fails, the application **still finishes starting up** (so the UI
|
||||
remains reachable to help you diagnose the problem), but:
|
||||
|
||||
- The failing migration is rolled back, leaving the database at the previous
|
||||
version. Any tables / indexes introduced by that migration **are not
|
||||
created**.
|
||||
- Downstream features depending on those tables (Wiki ingest, knowledge graph,
|
||||
task queues, …) may silently produce nothing.
|
||||
- The system info page shows the partial DB version + a red "Migration failed"
|
||||
tag and the captured error.
|
||||
|
||||
The cached error message you see in the UI is the same one logged at startup
|
||||
under `Database migration failed: ...`. Recent container logs are the
|
||||
authoritative source — copy them before doing anything destructive.
|
||||
|
||||
---
|
||||
|
||||
## Common causes
|
||||
|
||||
### 1. Missing PostgreSQL extension
|
||||
|
||||
Many migrations require extensions (`pg_trgm`, `vector`, `pg_search`) created
|
||||
by `CREATE EXTENSION IF NOT EXISTS`. **`IF NOT EXISTS` does not validate that
|
||||
the extension is actually installed** — it only checks the catalog. If the
|
||||
extension's shared library is missing or the role lacks `CREATE` privilege,
|
||||
the statement may succeed in the migration that nominally creates it but a
|
||||
later migration that uses the extension (e.g. building a `gin_trgm_ops` index)
|
||||
will fail.
|
||||
|
||||
**Symptoms in the error**:
|
||||
|
||||
```
|
||||
ERROR: operator class "gin_trgm_ops" does not exist for access method "gin"
|
||||
ERROR: type "vector" does not exist
|
||||
ERROR: function ... does not exist
|
||||
```
|
||||
|
||||
**Fix**:
|
||||
|
||||
```sql
|
||||
-- Connect as a superuser (typically `postgres`):
|
||||
\c your_weknora_database
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
CREATE EXTENSION IF NOT EXISTS vector; -- if RETRIEVE_DRIVER includes pgvector
|
||||
CREATE EXTENSION IF NOT EXISTS pg_search; -- only on ParadeDB
|
||||
|
||||
-- Verify they are actually loaded:
|
||||
SELECT extname, extversion FROM pg_extension WHERE extname IN ('pg_trgm','vector','pg_search');
|
||||
```
|
||||
|
||||
Then restart WeKnora. The next startup will pick up where the failing
|
||||
migration left off.
|
||||
|
||||
If `CREATE EXTENSION` itself errors with **"could not open extension control
|
||||
file"** or **"permission denied"**, the extension is not installed on your
|
||||
PostgreSQL server — install the corresponding OS package (e.g.
|
||||
`postgresql-contrib` for `pg_trgm`) or switch to an image that ships it
|
||||
preinstalled, then retry.
|
||||
|
||||
### 2. Dirty migration state
|
||||
|
||||
If a migration crashed partway through (OOM, container kill, network blip)
|
||||
`golang-migrate` marks the schema as "dirty" at the failing version. By
|
||||
default, WeKnora's startup tries to auto-recover; if you disabled that with
|
||||
`AUTO_RECOVER_DIRTY=false` you'll see:
|
||||
|
||||
```
|
||||
database is in dirty state at version N. ...
|
||||
```
|
||||
|
||||
**Fix** — use the bundled helpers:
|
||||
|
||||
```bash
|
||||
# Check the recorded version
|
||||
make migrate-version
|
||||
|
||||
# Force the version to the last successful migration (N - 1 in the message)
|
||||
make migrate-force version=<N-1>
|
||||
|
||||
# Re-run pending migrations
|
||||
make migrate-up
|
||||
```
|
||||
|
||||
After that, restart WeKnora.
|
||||
|
||||
Or set `AUTO_RECOVER_DIRTY=true` (the default in recent versions) and just
|
||||
restart — startup will perform the same `force` + retry automatically.
|
||||
|
||||
### 3. Insufficient privileges on the database role
|
||||
|
||||
Some migrations create extensions or alter shared catalogs, which require
|
||||
either superuser or `CREATEROLE` / `CREATEDB`. Errors look like:
|
||||
|
||||
```
|
||||
ERROR: permission denied to create extension "pg_trgm"
|
||||
ERROR: must be owner of database ...
|
||||
```
|
||||
|
||||
**Fix**: grant the role used by `DB_USER` the necessary privileges, or
|
||||
pre-create the extensions / objects as a superuser ahead of time, then
|
||||
restart. The migration's `CREATE EXTENSION IF NOT EXISTS` will then no-op.
|
||||
|
||||
### 4. Out-of-disk during `CREATE INDEX`
|
||||
|
||||
GIN / pgvector indexes can require significant temporary space. Errors:
|
||||
|
||||
```
|
||||
ERROR: could not extend file ...: No space left on device
|
||||
ERROR: cannot create temporary tables in transaction
|
||||
```
|
||||
|
||||
**Fix**: free disk on the volume backing `PGDATA`, then restart. The
|
||||
migration will retry the index build.
|
||||
|
||||
### 5. Schema drift from manual edits
|
||||
|
||||
If you previously edited tables / columns by hand and a later migration
|
||||
expects the original shape, it will fail with mismatched-type errors. The
|
||||
safest recovery is to align the live schema with the previous successful
|
||||
migration's `*.up.sql` and then re-run pending migrations.
|
||||
|
||||
---
|
||||
|
||||
## Generic diagnostic checklist
|
||||
|
||||
1. **Read the full error**: the cached message in the UI is truncated only by
|
||||
your browser scroll — it is the complete `golang-migrate` error. The
|
||||
container log shows the same content with stack context.
|
||||
2. **Identify the failing migration**: the version number in the error (or
|
||||
`make migrate-version`) points to a file under `migrations/versioned/`.
|
||||
Open `migrations/versioned/<version>_*.up.sql` and look for the statement
|
||||
matching the error type (extension, index, function, foreign key, …).
|
||||
3. **Run the failing statement manually** against the DB using `psql`. The
|
||||
error will be far more specific than the migration wrapper's.
|
||||
4. **Fix the underlying cause** (install extension, fix privileges, free
|
||||
disk, …), then either:
|
||||
- Restart WeKnora and let auto-recovery retry; **or**
|
||||
- Run `make migrate-up` from a checkout to apply migrations outside the
|
||||
server process.
|
||||
5. **Verify**: the system info page should now show the DB version without
|
||||
the "Migration failed" tag, and the previously broken feature (Wiki, KG,
|
||||
…) should start producing output.
|
||||
|
||||
---
|
||||
|
||||
## Reporting an issue
|
||||
|
||||
If you've worked through the checklist and the migration still fails, please
|
||||
open an issue at:
|
||||
|
||||
<https://github.com/Tencent/WeKnora/issues/new?template=bug_report.yml>
|
||||
|
||||
Include:
|
||||
|
||||
- WeKnora version + commit ID (from the system info page).
|
||||
- The full error from the system info page (or container logs).
|
||||
- PostgreSQL version (`SELECT version();`) and how it was deployed (vanilla,
|
||||
ParadeDB, Aurora, Aliyun RDS, …).
|
||||
- The output of:
|
||||
```sql
|
||||
SELECT extname, extversion FROM pg_extension;
|
||||
```
|
||||
- Any non-default values of `RETRIEVE_DRIVER`, `AUTO_MIGRATE`, and
|
||||
`AUTO_RECOVER_DIRTY`.
|
||||
|
||||
The "Report issue" link on the system info page pre-fills a body with the
|
||||
captured error for you — clicking it is the fastest path.
|
||||
@@ -0,0 +1,14 @@
|
||||
# 旧系统迁移审计
|
||||
|
||||
迁移前必须对旧 SQLite 做只读校验。执行:
|
||||
|
||||
```sh
|
||||
LEGACY_DB_PATH="/Volumes/Extreme SSD/yawan-storage/yawan-workbench/server/data/workspace.db" \
|
||||
./scripts/legacy_migration_audit.sh > /tmp/legacy-migration-audit.json
|
||||
```
|
||||
|
||||
脚本只执行 `PRAGMA integrity_check`、文件哈希和业务表计数,不会修改源库,也不会写入 WeKnora。生产旧库当前盘点为 6 个用户、6 个知识库、62 个文件、215,587 个片段、63 个分类、15 个 Agent、9 个技能、39 个技能绑定、3 个画布、2 个任务。数字人和微信渠道为空;点数账户 3 个、交易 797 条。
|
||||
|
||||
首批导入顺序:用户/租户映射、知识库、分类、源文件;文件进入原生对象存储后,使用原生解析和索引流程重新生成分块、向量和图谱。旧 Agent、技能、画布和任务先导出为只读归档,待原生模块逐项映射后再导入。旧密码哈希和 API key 不迁移,用户需要重新设置凭据。
|
||||
|
||||
每批导入必须记录 `source_id -> native_id` 映射、成功/失败计数和可重试游标。验收通过前保留旧库和源文件的只读快照;回滚只删除本次导入批次的 native ID,不覆盖已有租户数据。
|
||||
@@ -0,0 +1,75 @@
|
||||
# Native Historical Evidence Graph
|
||||
|
||||
Acceptance database: `yawan_native_acceptance_20260915`, tenant `10059`.
|
||||
Source: read-only snapshot `/tmp/yawan-graph-source-20260917.sqlite`.
|
||||
Snapshot SHA-256: `2643990033bdde26a19729f114f3274600801d703476cfc8f339f572802e324f`.
|
||||
|
||||
## Preserved Records
|
||||
|
||||
Migration 200 stores original table names, IDs, graph version IDs, complete JSON payloads, tenant/knowledge-base ownership, and source snapshot hash in PostgreSQL. No model extraction is needed to retain the original graph. The old SQLite and production database were not modified.
|
||||
|
||||
| Original table | Preserved rows |
|
||||
| --- | ---: |
|
||||
| knowledgegraphversion | 5 |
|
||||
| knowledgeentity | 56,149 |
|
||||
| knowledgerelation | 7,627 |
|
||||
| knowledgegraphevidence | 178,985 |
|
||||
| knowledgegraphchunkstate | 107,452 |
|
||||
| knowledgegraphsourceunit | 0 |
|
||||
| knowledgegraphclaim | 0 |
|
||||
| knowledgeentityalias | 0 |
|
||||
| knowledgegraphcommunity | 0 |
|
||||
| knowledgegraphcommunityreport | 0 |
|
||||
| **Total** | **350,218** |
|
||||
|
||||
The zero-row tables were inspected and are empty in the snapshot. Import replay checks payload equality and table counts, refuses conflicting source records, and preserves existing verified mappings.
|
||||
|
||||
## Verified Native Mapping
|
||||
|
||||
There are 107,397 distinct source chunks referenced by preserved evidence. Of these, 17,025 have verified native counterparts; 90,372 remain available as original evidence without a native chunk link. The verified sources back 24,077 of 178,985 evidence rows. The separate chunk-state table contains 107,452 historical state records, including sources not referenced by evidence.
|
||||
|
||||
| Knowledge base | Evidence source chunks | Native mappings |
|
||||
| --- | ---: | ---: |
|
||||
| 产品信息 | 37 | 3 |
|
||||
| 功法 | 9,801 | 2,073 |
|
||||
| 心法 | 66,204 | 7,000 |
|
||||
| 技法 | 17,154 | 4,601 |
|
||||
| 销售逻辑 | 14,201 | 3,348 |
|
||||
|
||||
Exact raw SHA-256 matching produced zero matches because native ingestion changed chunk boundaries. The successful mapping requires a uniquely identified native source document in the same tenant and knowledge base, then a unique native chunk containing the entire normalized historical excerpt. Normalization is Unicode NFKC plus whitespace removal; excerpts shorter than 80 Unicode codepoints are excluded. Filename conversion from `.jsonl` to `.md` restricts candidate documents only and is never sufficient to map a chunk. Duplicate candidate documents and ambiguous chunk matches are left unmapped; the initial pass found 72 ambiguous excerpt matches.
|
||||
|
||||
Each mapping stores the current native chunk SHA-256. Evidence links and retrieval recheck that hash, chunk enablement, and live chunk/document/knowledge-base ownership. Modified, disabled, or deleted resources lose their usable link. Overview counts describe migration-time associations, as the UI explicitly labels them; they do not rescan and hash the full corpus on each click.
|
||||
|
||||
## Retrieval And UI
|
||||
|
||||
`LEGACY_GRAPH_ENABLE=true` activates the historical PostgreSQL graph adapter through the existing graph retrieval interface. Neo4j may remain disabled. A configured Neo4j driver is still supported for existing graph writes, deletes, and additional retrieval. Historical records remain archived when native documents are removed; unavailable native links are not returned.
|
||||
|
||||
Search accepts up to eight terms of 200 codepoints, selects at most 16 seeds, expands at most 16 neighboring entities and 32 relations, then fetches up to four verified chunks per entity in a single bounded evidence query. Terms are matched against preserved canonical entity names. The original entity labels and extraction quality are preserved; this migration does not invent new labels or relationships. Legacy chunk IDs are never passed into native RAG.
|
||||
|
||||
The authenticated endpoint is `GET /api/v1/knowledge-bases/:id/evidence-graph`. It uses existing viewer, knowledge-base read, and scoped API-key guards. The knowledge-base Evidence Graph tab supports entity search, pagination, original evidence, relationships, mapping status, and native document opening independently of Wiki.
|
||||
|
||||
## Reproduction
|
||||
|
||||
The scripts restrict writes to isolated acceptance databases matching `yawan_native_acceptance_[0-9]+`; their default is the database above. The Docker context is `colima-yawan`, container `WeKnora-postgres`.
|
||||
|
||||
```sh
|
||||
node scripts/migrate_native_graph.mjs /tmp/yawan-graph-source-20260917.sqlite --apply
|
||||
node scripts/map_native_graph_evidence.mjs --apply
|
||||
node scripts/verify_native_graph.mjs
|
||||
node scripts/verify_native_graph_ui.cjs
|
||||
```
|
||||
|
||||
The UI verifier requires Node 20+, the bundled Playwright package and installed Chrome. It reads the private acceptance login file without printing credentials and targets API port 18080 and frontend port 15173. No credentials are stored in this document.
|
||||
|
||||
## Validation
|
||||
|
||||
- Import replay passed with identical counts and all 17,025 mappings retained.
|
||||
- PostgreSQL integration tests passed: missing scope, tenant isolation, document scope, unchanged native retrieval, relation-neighbor expansion, bounded results, and rolled-back modified/disabled/deleted resource fixtures.
|
||||
- Largest knowledge-base overview measured 152-237 ms after removing corpus-wide per-request hashing; representative graph retrieval measured 117-238 ms. These are local acceptance observations, not a production performance guarantee.
|
||||
- Graph source count query uses an index-only scan; a measured 66,204-source count took 8.6 ms. The migration includes its covering index and replay performs VACUUM ANALYZE after commit.
|
||||
- Browser verification passed at 1440x1000 and 390x844: authorized 200 and unauthenticated 401, entity details, mapped and unmapped evidence, native document drawer, empty search and reset, no erroneous empty-document guide, no browser errors, and no mobile horizontal overflow.
|
||||
- `vue-tsc --noEmit` passed. Targeted Go package compilation and chat-pipeline tests passed.
|
||||
|
||||
Browser artifacts are in `/Volumes/Extreme SSD/yawan-storage/weknora/acceptance/`: `native-graph-desktop.png`, `native-graph-mobile.png`, and `native-graph-ui-verification.json`. Import replay log: `/tmp/native-graph-replay-final.log`.
|
||||
|
||||
The latest repository batching and availability-filter changes require the parent-managed API rebuild. Frontend changes are served by the existing dev server. This task did not restart the acceptance API.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Docker 沙箱后端可行性 PoC
|
||||
|
||||
配套文档:[Docker 沙箱后端重做调研](../../sandbox-docker-backend.md)。
|
||||
|
||||
这个程序直接打 Docker Engine API,逐项验证「Docker 能不能承载 WeKnora
|
||||
`RemoteSandboxClient` 契约 + E2B 那套 Snapshot 工作流」。它不是产品代码,也不参与主模块构建
|
||||
(自带 `go.mod`),只作为调研结论的可复现证据。
|
||||
|
||||
每一项都打印 `PASS/FAIL` 和实际观测值。带 `(GAP)` 的步骤断言的是「Docker 做不到什么」,
|
||||
它们同样以 PASS 结束——PASS 表示差距被复现了,不是表示能力具备。
|
||||
|
||||
## 跑法
|
||||
|
||||
需要一个可达的 Docker daemon(尊重 `DOCKER_HOST`),能拉 `python:3.11-slim`:
|
||||
|
||||
```bash
|
||||
cd docs/poc/docker-sandbox
|
||||
go run . # daemon 在本机 unix socket 上时可能需要 sudo -E
|
||||
```
|
||||
|
||||
程序会自建一个带 uid 1000 `user` 账号的模板镜像(对齐 E2B 模板约定),跑完自行删除它创建的容器。
|
||||
留下的 `weknora-poc/*` 镜像用 `docker image rm` 清理。
|
||||
|
||||
## 覆盖的内容
|
||||
|
||||
- 生命周期:Create / Connect(换客户端重连)/ List(label 过滤)/ Delete / Pause / Stop+Start
|
||||
- 执行:user、workdir、env、stdin、stdout+stderr 分离、退出码、超时
|
||||
- 文件面:archive API 读写与 stat、exec 实现的 mkdir/ls/rm
|
||||
- 会话语义:`pip install` 与写文件跨 exec 存活
|
||||
- Snapshot:commit 成镜像、从快照启新沙箱、v1→v2 增量、按 label 列举、层数与体积
|
||||
- 差距复现:客户端取消不杀进程、CapDrop ALL 后 root 越不过权限位、快照不含内存态、
|
||||
daemon 无空闲 TTL、kill `docker run` 会留下还在跑的容器
|
||||
@@ -0,0 +1,29 @@
|
||||
module github.com/Tencent/WeKnora/docs/poc/docker-sandbox
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
github.com/moby/moby/api v1.55.0
|
||||
github.com/moby/moby/client v0.5.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.7.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,61 @@
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
|
||||
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc=
|
||||
github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
||||
github.com/moby/moby/client v0.5.1 h1:tYNaJno4c0HXz12y5BiqEDy0rVTYkWzI26lGvnTMiJw=
|
||||
github.com/moby/moby/client v0.5.1/go.mod h1:odLstlZ6uSnfvAgVxMpvgmb8SUdd+siH2T0GBuxVAlM=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
|
||||
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
||||
@@ -0,0 +1,612 @@
|
||||
// PoC for docs/sandbox-docker-backend.md: can the Docker Engine API back the
|
||||
// semantics WeKnora's RemoteSandboxClient contract requires (session-persistent
|
||||
// sandbox, exec, filesystem, metadata, lifecycle), plus the snapshot workflow
|
||||
// planned for E2B?
|
||||
//
|
||||
// It drives a real daemon and prints PASS/FAIL plus the observed evidence for
|
||||
// every operation, including the ones Docker cannot honour ("GAP" steps, which
|
||||
// assert the gap rather than a capability).
|
||||
//
|
||||
// Run with a reachable daemon (DOCKER_HOST honoured):
|
||||
//
|
||||
// cd docs/poc/docker-sandbox && go run .
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/moby/moby/api/pkg/stdcopy"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
)
|
||||
|
||||
const (
|
||||
baseImage = "python:3.11-slim"
|
||||
execUser = "user"
|
||||
)
|
||||
|
||||
var failures int
|
||||
|
||||
func step(name string, err error, detail string) {
|
||||
if err != nil {
|
||||
failures++
|
||||
fmt.Printf("FAIL %-48s %v\n", name, err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("PASS %-48s %s\n", name, detail)
|
||||
}
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
cli, err := client.New(client.WithHostFromEnv(), client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
fmt.Println("cannot reach docker:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
ping, err := cli.Ping(ctx, client.PingOptions{})
|
||||
step("Health: ping control plane", err, fmt.Sprintf("api=%s", ping.APIVersion))
|
||||
|
||||
tmplImage := "weknora-poc/template:v2"
|
||||
err = buildTemplateImage(ctx, cli, tmplImage)
|
||||
step("Template: base image with uid-1000 user", err, tmplImage)
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
sessionID := fmt.Sprintf("poc-session-%d", time.Now().Unix())
|
||||
labels := map[string]string{
|
||||
"weknora.managed": "true",
|
||||
"weknora.tenant": "1",
|
||||
"weknora.session": sessionID,
|
||||
"weknora.config": "cfg-poc",
|
||||
"weknora.createdAt": time.Now().UTC().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
// --- Create -------------------------------------------------------------
|
||||
id, err := createSandbox(ctx, cli, tmplImage, labels)
|
||||
step("Create: container as session sandbox", err, short(id))
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
defer remove(cli, id)
|
||||
|
||||
// --- Connect / Get ------------------------------------------------------
|
||||
fresh, err := client.New(client.WithHostFromEnv(), client.WithAPIVersionNegotiation())
|
||||
if err == nil {
|
||||
defer fresh.Close()
|
||||
var insp client.ContainerInspectResult
|
||||
insp, err = fresh.ContainerInspect(ctx, id, client.ContainerInspectOptions{})
|
||||
if err == nil && insp.Container.Config.Labels["weknora.session"] != sessionID {
|
||||
err = errors.New("metadata labels not preserved")
|
||||
}
|
||||
step("Connect+Metadata: re-attach from a new client", err,
|
||||
fmt.Sprintf("state=%s session=%s", insp.Container.State.Status,
|
||||
insp.Container.Config.Labels["weknora.session"]))
|
||||
}
|
||||
|
||||
// --- List by metadata ---------------------------------------------------
|
||||
listed, err := cli.ContainerList(ctx, client.ContainerListOptions{
|
||||
All: true,
|
||||
Filters: client.Filters{}.Add("label", "weknora.session="+sessionID),
|
||||
})
|
||||
if err == nil && len(listed.Items) != 1 {
|
||||
err = fmt.Errorf("expected 1 container, got %d", len(listed.Items))
|
||||
}
|
||||
step("List: server-side metadata filter", err, fmt.Sprintf("matched=%d", len(listed.Items)))
|
||||
|
||||
// --- Exec: user / workdir / env / stdin / demux / exit code -------------
|
||||
res, err := execIn(ctx, cli, id, execRequest{
|
||||
cmd: []string{"bash", "-lc", `cat; echo "user=$(id -un) cwd=$(pwd) env=$GREET"; echo oops >&2; exit 7`},
|
||||
user: execUser,
|
||||
workDir: "/workspace",
|
||||
env: []string{"GREET=hello"},
|
||||
stdin: "stdin-payload\n",
|
||||
})
|
||||
if err == nil {
|
||||
switch {
|
||||
case res.exitCode != 7:
|
||||
err = fmt.Errorf("exit code %d, want 7", res.exitCode)
|
||||
case !strings.Contains(res.stdout, "user=user cwd=/workspace env=hello"):
|
||||
err = fmt.Errorf("stdout mismatch: %q", res.stdout)
|
||||
case !strings.Contains(res.stdout, "stdin-payload"):
|
||||
err = fmt.Errorf("stdin not delivered: %q", res.stdout)
|
||||
case !strings.Contains(res.stderr, "oops"):
|
||||
err = fmt.Errorf("stderr not separated: %q", res.stderr)
|
||||
}
|
||||
}
|
||||
step("Exec: user/workdir/env/stdin/demux/exit", err,
|
||||
fmt.Sprintf("exit=%d stdout=%q stderr=%q", res.exitCode, oneline(res.stdout), oneline(res.stderr)))
|
||||
|
||||
// --- Exec cancellation: does a client-side cancel kill the process? -----
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
||||
_, execErr := execIn(cancelCtx, cli, id, execRequest{
|
||||
cmd: []string{"bash", "-lc", "sleep 30"},
|
||||
})
|
||||
cancel()
|
||||
time.Sleep(1 * time.Second)
|
||||
probe, _ := execIn(ctx, cli, id, execRequest{
|
||||
cmd: []string{"bash", "-lc", "ps -eo args= | grep -c '^sleep 30$' || true"},
|
||||
})
|
||||
step("Exec cancel: client cancel does NOT kill process (GAP)", nil,
|
||||
fmt.Sprintf("clientErr=%v surviving 'sleep' procs=%s", execErr != nil, oneline(probe.stdout)))
|
||||
|
||||
// --- Timeout enforced inside the container ------------------------------
|
||||
start := time.Now()
|
||||
killRes, err := execIn(ctx, cli, id, execRequest{
|
||||
cmd: []string{"timeout", "-s", "KILL", "2", "bash", "-lc", "sleep 30"},
|
||||
})
|
||||
elapsed := time.Since(start)
|
||||
if err == nil && killRes.exitCode != 137 {
|
||||
err = fmt.Errorf("exit code %d, want 137 (SIGKILL)", killRes.exitCode)
|
||||
}
|
||||
step("Timeout: enforce via in-container timeout(1)", err,
|
||||
fmt.Sprintf("exit=%d after=%s", killRes.exitCode, elapsed.Round(100*time.Millisecond)))
|
||||
|
||||
// --- Filesystem: write / read / stat / mkdir / list / remove ------------
|
||||
err = writeFile(ctx, cli, id, "/workspace/input/attachment.txt", []byte("attachment payload\n"))
|
||||
step("WriteFile: PUT /containers/{id}/archive", err, "/workspace/input/attachment.txt")
|
||||
|
||||
content, err := readFile(ctx, cli, id, "/workspace/input/attachment.txt")
|
||||
if err == nil && string(content) != "attachment payload\n" {
|
||||
err = fmt.Errorf("content mismatch: %q", content)
|
||||
}
|
||||
step("ReadFile: GET /containers/{id}/archive", err, fmt.Sprintf("%d bytes", len(content)))
|
||||
|
||||
stat, err := cli.ContainerStatPath(ctx, id, client.ContainerStatPathOptions{
|
||||
Path: "/workspace/input/attachment.txt",
|
||||
})
|
||||
step("Stat: HEAD /containers/{id}/archive", err,
|
||||
fmt.Sprintf("size=%d mode=%v mtime=%s", stat.Stat.Size, stat.Stat.Mode.Perm(),
|
||||
stat.Stat.Mtime.Format(time.RFC3339)))
|
||||
|
||||
rootMk, _ := execIn(ctx, cli, id, execRequest{cmd: []string{"mkdir", "-p", "/workspace/output/rootdir"}})
|
||||
step("CapDrop ALL: root cannot bypass mode bits (GAP)", nil,
|
||||
fmt.Sprintf("mkdir as root exit=%d stderr=%q", rootMk.exitCode, oneline(rootMk.stderr)))
|
||||
|
||||
_, err = mustExec(ctx, cli, id, execRequest{
|
||||
cmd: []string{"mkdir", "-p", "/workspace/output/nested"},
|
||||
user: execUser,
|
||||
})
|
||||
step("MakeDir: exec mkdir -p as sandbox user", err, "/workspace/output/nested")
|
||||
|
||||
if _, err = mustExec(ctx, cli, id, execRequest{
|
||||
cmd: []string{"bash", "-lc", "echo body > /workspace/output/nested/report.txt"},
|
||||
user: execUser,
|
||||
}); err != nil {
|
||||
step("ListDir: seed a file to list", err, "")
|
||||
}
|
||||
lsRes, err := mustExec(ctx, cli, id, execRequest{
|
||||
cmd: []string{"find", "/workspace/output", "-mindepth", "1", "-printf", `%y\t%s\t%T@\t%p\n`},
|
||||
user: execUser,
|
||||
})
|
||||
if err == nil && !strings.Contains(lsRes.stdout, "report.txt") {
|
||||
err = fmt.Errorf("listing missing report.txt: %q", lsRes.stdout)
|
||||
}
|
||||
step("ListDir: exec find (no native API)", err,
|
||||
fmt.Sprintf("exit=%d out=%q err=%q", lsRes.exitCode, oneline(lsRes.stdout), oneline(lsRes.stderr)))
|
||||
|
||||
_, err = mustExec(ctx, cli, id, execRequest{
|
||||
cmd: []string{"rm", "-rf", "/workspace/input/attachment.txt"},
|
||||
user: execUser,
|
||||
})
|
||||
step("Remove: exec rm (no native API)", err, "removed")
|
||||
|
||||
// --- Session-scoped state persists across executions --------------------
|
||||
installRes, err := execIn(ctx, cli, id, execRequest{
|
||||
cmd: []string{"bash", "-lc", "pip install --quiet --no-cache-dir --user cowsay==6.1 && echo 1 > /workspace/output/counter.txt"},
|
||||
user: execUser,
|
||||
})
|
||||
if err == nil && installRes.exitCode != 0 {
|
||||
err = fmt.Errorf("install failed: %s %s", oneline(installRes.stdout), oneline(installRes.stderr))
|
||||
}
|
||||
if err == nil {
|
||||
var second execResult
|
||||
second, err = execIn(ctx, cli, id, execRequest{
|
||||
cmd: []string{"bash", "-lc", `python -c 'import cowsay; print("cowsay ok")' && cat /workspace/output/counter.txt`},
|
||||
user: execUser,
|
||||
})
|
||||
if err == nil && (!strings.Contains(second.stdout, "cowsay ok") || !strings.Contains(second.stdout, "1")) {
|
||||
err = fmt.Errorf("state lost between executions: %q %q", second.stdout, second.stderr)
|
||||
}
|
||||
}
|
||||
step("Session state: pip install survives next exec", err, "package + file both visible")
|
||||
|
||||
// --- Pause / Unpause (memory kept resident) -----------------------------
|
||||
_, err = cli.ContainerPause(ctx, id, client.ContainerPauseOptions{})
|
||||
if err == nil {
|
||||
insp, _ := cli.ContainerInspect(ctx, id, client.ContainerInspectOptions{})
|
||||
if insp.Container.State.Status != "paused" {
|
||||
err = fmt.Errorf("state=%s", insp.Container.State.Status)
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
_, err = cli.ContainerUnpause(ctx, id, client.ContainerUnpauseOptions{})
|
||||
}
|
||||
if err == nil {
|
||||
var after execResult
|
||||
after, err = execIn(ctx, cli, id, execRequest{cmd: []string{"cat", "/workspace/output/counter.txt"}})
|
||||
if err == nil && !strings.Contains(after.stdout, "1") {
|
||||
err = fmt.Errorf("state lost after unpause: %q", after.stdout)
|
||||
}
|
||||
}
|
||||
step("Pause/Unpause: cgroup freezer round-trip", err,
|
||||
"RAM + processes retained, host memory still occupied")
|
||||
|
||||
// --- Stop / Start (filesystem kept, processes lost) ---------------------
|
||||
_, _ = execIn(ctx, cli, id, execRequest{cmd: []string{"bash", "-lc", "nohup sleep 900 >/dev/null 2>&1 & echo started"}})
|
||||
timeoutSec := 5
|
||||
_, err = cli.ContainerStop(ctx, id, client.ContainerStopOptions{Timeout: &timeoutSec})
|
||||
if err == nil {
|
||||
_, err = cli.ContainerStart(ctx, id, client.ContainerStartOptions{})
|
||||
}
|
||||
var afterRestart execResult
|
||||
if err == nil {
|
||||
afterRestart, err = execIn(ctx, cli, id, execRequest{
|
||||
cmd: []string{"bash", "-lc", "cat /workspace/output/counter.txt; ps -eo args= | grep -c '^sleep 900$' || true"},
|
||||
})
|
||||
if err == nil && !strings.Contains(afterRestart.stdout, "1") {
|
||||
err = fmt.Errorf("filesystem lost after restart: %q", afterRestart.stdout)
|
||||
}
|
||||
}
|
||||
step("Stop/Start: filesystem survives, processes do not", err,
|
||||
fmt.Sprintf("counter+sleep-count=%s", oneline(afterRestart.stdout)))
|
||||
|
||||
// --- Snapshot workflow --------------------------------------------------
|
||||
mgmtID, err := createSandbox(ctx, cli, tmplImage,
|
||||
map[string]string{"weknora.managed": "true", "weknora.role": "snapshot-builder"})
|
||||
step("Snapshot: create workspace management sandbox", err, short(mgmtID))
|
||||
if err != nil {
|
||||
report()
|
||||
return
|
||||
}
|
||||
defer remove(cli, mgmtID)
|
||||
|
||||
skillRes, err := execIn(ctx, cli, mgmtID, execRequest{
|
||||
cmd: []string{"bash", "-lc", "pip install --quiet --no-cache-dir requests==2.32.3 && mkdir -p /opt/skills/pdf && echo 'skill v1' > /opt/skills/pdf/SKILL.md"},
|
||||
})
|
||||
if err == nil && skillRes.exitCode != 0 {
|
||||
err = fmt.Errorf("skill install failed: %s", oneline(skillRes.stderr))
|
||||
}
|
||||
step("Snapshot: install a skill into that sandbox", err, "requests==2.32.3 + /opt/skills/pdf")
|
||||
|
||||
snapV1 := "weknora-poc/snapshot:v1"
|
||||
commitRes, err := cli.ContainerCommit(ctx, mgmtID, client.ContainerCommitOptions{
|
||||
Reference: snapV1,
|
||||
Comment: "weknora snapshot v1",
|
||||
Changes: []string{`LABEL weknora.snapshot.version=1`, `LABEL weknora.snapshot.tenant=1`},
|
||||
})
|
||||
step("Snapshot: commit container -> image", err, short(commitRes.ID))
|
||||
|
||||
snapSessionID, err := createSandbox(ctx, cli, snapV1,
|
||||
map[string]string{"weknora.managed": "true", "weknora.session": sessionID + "-from-snapshot"})
|
||||
step("Snapshot: session sandbox boots from snapshot", err, short(snapSessionID))
|
||||
if err == nil {
|
||||
defer remove(cli, snapSessionID)
|
||||
var check execResult
|
||||
check, err = execIn(ctx, cli, snapSessionID, execRequest{
|
||||
cmd: []string{"bash", "-lc", `python -c 'import requests; print(requests.__version__)' && cat /opt/skills/pdf/SKILL.md`},
|
||||
})
|
||||
if err == nil && (!strings.Contains(check.stdout, "2.32.3") || !strings.Contains(check.stdout, "skill v1")) {
|
||||
err = fmt.Errorf("snapshot content missing: %q %q", check.stdout, check.stderr)
|
||||
}
|
||||
step("Snapshot: installed skill present in new sandbox", err, oneline(check.stdout))
|
||||
}
|
||||
|
||||
// Incremental snapshot update: install a second skill on top of v1.
|
||||
upgradeID, err := createSandbox(ctx, cli, snapV1, map[string]string{"weknora.role": "snapshot-builder"})
|
||||
if err == nil {
|
||||
defer remove(cli, upgradeID)
|
||||
_, err = execIn(ctx, cli, upgradeID, execRequest{
|
||||
cmd: []string{"bash", "-lc", "mkdir -p /opt/skills/chart && echo 'skill v2' > /opt/skills/chart/SKILL.md"},
|
||||
})
|
||||
}
|
||||
snapV2 := "weknora-poc/snapshot:v2"
|
||||
if err == nil {
|
||||
_, err = cli.ContainerCommit(ctx, upgradeID, client.ContainerCommitOptions{
|
||||
Reference: snapV2,
|
||||
Changes: []string{`LABEL weknora.snapshot.version=2`},
|
||||
})
|
||||
}
|
||||
step("Snapshot: incremental update v1 -> v2", err, snapV2)
|
||||
|
||||
imgs, err := cli.ImageList(ctx, client.ImageListOptions{
|
||||
All: true,
|
||||
Filters: client.Filters{}.Add("label", "weknora.snapshot.version"),
|
||||
})
|
||||
step("Snapshot: list snapshots by label", err, fmt.Sprintf("count=%d", len(imgs.Items)))
|
||||
|
||||
insp, err := cli.ImageInspect(ctx, snapV2)
|
||||
if err == nil {
|
||||
step("Snapshot: layer/size accounting", nil,
|
||||
fmt.Sprintf("layers=%d size=%.1fMB (image layer hard limit: 127)",
|
||||
len(insp.RootFS.Layers), float64(insp.Size)/1e6))
|
||||
} else {
|
||||
step("Snapshot: layer/size accounting", err, "")
|
||||
}
|
||||
|
||||
// Does a snapshot preserve running processes / RAM? (E2B pause does.)
|
||||
_, _ = execIn(ctx, cli, mgmtID, execRequest{cmd: []string{"bash", "-lc", "nohup sleep 600 >/dev/null 2>&1 & echo started"}})
|
||||
procSnap := "weknora-poc/snapshot:proc"
|
||||
_, err = cli.ContainerCommit(ctx, mgmtID, client.ContainerCommitOptions{Reference: procSnap})
|
||||
if err == nil {
|
||||
var procID string
|
||||
procID, err = createSandbox(ctx, cli, procSnap, map[string]string{"weknora.role": "proc-check"})
|
||||
if err == nil {
|
||||
defer remove(cli, procID)
|
||||
var out execResult
|
||||
out, err = execIn(ctx, cli, procID, execRequest{cmd: []string{"bash", "-lc", "ps -eo args= | grep -c '^sleep 600$' || true"}})
|
||||
step("Snapshot: running processes NOT captured (GAP)", err,
|
||||
fmt.Sprintf("'sleep' procs in restored sandbox=%s", oneline(out.stdout)))
|
||||
}
|
||||
}
|
||||
|
||||
insp2, err := cli.ContainerInspect(ctx, id, client.ContainerInspectOptions{})
|
||||
step("Idle TTL: daemon has none, only timestamps (GAP)", err,
|
||||
fmt.Sprintf("startedAt=%s -> WeKnora must sweep", insp2.Container.State.StartedAt))
|
||||
|
||||
checkCLIOrphan(ctx)
|
||||
|
||||
report()
|
||||
}
|
||||
|
||||
// checkCLIOrphan reproduces what today's CLI-based DockerSandbox does on
|
||||
// timeout: exec.CommandContext kills the `docker run --rm` client process. The
|
||||
// workload lives in the daemon, so killing the client does not stop it — the
|
||||
// timeout is reported to the caller while the container keeps burning CPU.
|
||||
func checkCLIOrphan(ctx context.Context) {
|
||||
if _, err := exec.LookPath("docker"); err != nil {
|
||||
step("CLI orphan: skipped (docker CLI not on PATH)", nil, "")
|
||||
return
|
||||
}
|
||||
name := fmt.Sprintf("weknora-poc-orphan-%d", time.Now().UnixNano())
|
||||
runCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(runCtx, "docker", "run", "--rm", "--name", name,
|
||||
baseImage, "sleep", "120")
|
||||
cliErr := cmd.Run()
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
out, err := exec.CommandContext(ctx, "docker", "ps", "--filter", "name="+name,
|
||||
"--format", "{{.State}} {{.Status}}").Output()
|
||||
state := oneline(string(out))
|
||||
_ = exec.CommandContext(ctx, "docker", "rm", "-f", name).Run()
|
||||
if state == "" {
|
||||
state = "gone"
|
||||
}
|
||||
step("CLI orphan: killing `docker run` leaves it running (GAP)", err,
|
||||
fmt.Sprintf("cliErr=%v container=%s", cliErr != nil, state))
|
||||
}
|
||||
|
||||
func report() {
|
||||
fmt.Println()
|
||||
if failures == 0 {
|
||||
fmt.Println("RESULT: all checks passed")
|
||||
return
|
||||
}
|
||||
fmt.Printf("RESULT: %d check(s) failed\n", failures)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// --- helpers -----------------------------------------------------------------
|
||||
|
||||
func remove(cli *client.Client, id string) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
_, _ = cli.ContainerRemove(ctx, id, client.ContainerRemoveOptions{Force: true})
|
||||
}
|
||||
|
||||
func buildTemplateImage(ctx context.Context, cli *client.Client, tag string) error {
|
||||
if _, err := cli.ImageInspect(ctx, tag); err == nil {
|
||||
return nil
|
||||
}
|
||||
dockerfile := `FROM ` + baseImage + `
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends procps findutils && rm -rf /var/lib/apt/lists/*
|
||||
RUN useradd -m -u 1000 user && mkdir -p /workspace/input /workspace/output && chown -R user:user /workspace
|
||||
WORKDIR /workspace
|
||||
`
|
||||
var buf bytes.Buffer
|
||||
tw := tar.NewWriter(&buf)
|
||||
if err := tw.WriteHeader(&tar.Header{Name: "Dockerfile", Mode: 0o644, Size: int64(len(dockerfile))}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tw.Write([]byte(dockerfile)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tw.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := cli.ImageBuild(ctx, &buf, client.ImageBuildOptions{Tags: []string{tag}, Remove: true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
_, err = io.Copy(io.Discard, resp.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
func createSandbox(ctx context.Context, cli *client.Client, img string, labels map[string]string) (string, error) {
|
||||
pids := int64(256)
|
||||
created, err := cli.ContainerCreate(ctx, client.ContainerCreateOptions{
|
||||
Image: img,
|
||||
Config: &container.Config{
|
||||
Cmd: []string{"sleep", "infinity"},
|
||||
WorkingDir: "/workspace",
|
||||
Labels: labels,
|
||||
},
|
||||
HostConfig: &container.HostConfig{
|
||||
Resources: container.Resources{
|
||||
Memory: 1 << 30,
|
||||
MemorySwap: 1 << 30,
|
||||
NanoCPUs: 1_000_000_000,
|
||||
PidsLimit: &pids,
|
||||
},
|
||||
CapDrop: []string{"ALL"},
|
||||
SecurityOpt: []string{"no-new-privileges"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := cli.ContainerStart(ctx, created.ID, client.ContainerStartOptions{}); err != nil {
|
||||
return created.ID, err
|
||||
}
|
||||
return created.ID, nil
|
||||
}
|
||||
|
||||
type execRequest struct {
|
||||
cmd []string
|
||||
user string
|
||||
workDir string
|
||||
env []string
|
||||
stdin string
|
||||
}
|
||||
|
||||
type execResult struct {
|
||||
stdout string
|
||||
stderr string
|
||||
exitCode int
|
||||
}
|
||||
|
||||
func execIn(ctx context.Context, cli *client.Client, id string, req execRequest) (execResult, error) {
|
||||
created, err := cli.ExecCreate(ctx, id, client.ExecCreateOptions{
|
||||
Cmd: req.cmd,
|
||||
User: req.user,
|
||||
WorkingDir: req.workDir,
|
||||
Env: req.env,
|
||||
AttachStdin: req.stdin != "",
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
})
|
||||
if err != nil {
|
||||
return execResult{}, err
|
||||
}
|
||||
attached, err := cli.ExecAttach(ctx, created.ID, client.ExecAttachOptions{})
|
||||
if err != nil {
|
||||
return execResult{}, err
|
||||
}
|
||||
defer attached.Close()
|
||||
|
||||
if req.stdin != "" {
|
||||
if _, err := attached.Conn.Write([]byte(req.stdin)); err != nil {
|
||||
return execResult{}, err
|
||||
}
|
||||
if cw, ok := attached.Conn.(interface{ CloseWrite() error }); ok {
|
||||
_ = cw.CloseWrite()
|
||||
}
|
||||
}
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
copyDone := make(chan error, 1)
|
||||
go func() {
|
||||
_, copyErr := stdcopy.StdCopy(&stdout, &stderr, attached.Reader)
|
||||
copyDone <- copyErr
|
||||
}()
|
||||
select {
|
||||
case err = <-copyDone:
|
||||
if err != nil {
|
||||
return execResult{}, err
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return execResult{stdout: stdout.String(), stderr: stderr.String()}, ctx.Err()
|
||||
}
|
||||
|
||||
inspect, err := cli.ExecInspect(ctx, created.ID, client.ExecInspectOptions{})
|
||||
if err != nil {
|
||||
return execResult{}, err
|
||||
}
|
||||
return execResult{
|
||||
stdout: stdout.String(),
|
||||
stderr: stderr.String(),
|
||||
exitCode: inspect.ExitCode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// mustExec fails when the command itself exits non-zero, so a silently
|
||||
// failing setup step cannot be mistaken for a passing capability check.
|
||||
func mustExec(ctx context.Context, cli *client.Client, id string, req execRequest) (execResult, error) {
|
||||
res, err := execIn(ctx, cli, id, req)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
if res.exitCode != 0 {
|
||||
return res, fmt.Errorf("exit=%d stderr=%s", res.exitCode, oneline(res.stderr))
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func writeFile(ctx context.Context, cli *client.Client, id, dest string, content []byte) error {
|
||||
if _, err := execIn(ctx, cli, id, execRequest{cmd: []string{"mkdir", "-p", dirOf(dest)}}); err != nil {
|
||||
return err
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
tw := tar.NewWriter(&buf)
|
||||
name := dest[strings.LastIndex(dest, "/")+1:]
|
||||
if err := tw.WriteHeader(&tar.Header{
|
||||
Name: name, Mode: 0o644, Size: int64(len(content)), Uid: 1000, Gid: 1000,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tw.Write(content); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tw.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := cli.CopyToContainer(ctx, id, client.CopyToContainerOptions{
|
||||
DestinationPath: dirOf(dest),
|
||||
Content: &buf,
|
||||
CopyUIDGID: true,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func readFile(ctx context.Context, cli *client.Client, id, src string) ([]byte, error) {
|
||||
res, err := cli.CopyFromContainer(ctx, id, client.CopyFromContainerOptions{SourcePath: src})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Content.Close()
|
||||
tr := tar.NewReader(res.Content)
|
||||
for {
|
||||
header, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
return nil, errors.New("file not present in archive")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if header.Typeflag == tar.TypeReg {
|
||||
return io.ReadAll(tr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func dirOf(p string) string {
|
||||
idx := strings.LastIndex(p, "/")
|
||||
if idx <= 0 {
|
||||
return "/"
|
||||
}
|
||||
return p[:idx]
|
||||
}
|
||||
|
||||
func short(id string) string {
|
||||
if len(id) > 12 {
|
||||
return id[:12]
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func oneline(s string) string {
|
||||
return strings.Join(strings.Fields(s), " ")
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
# WeKnora 沙箱集群与标准模板
|
||||
|
||||
本文面向部署和平台管理员,说明如何把 CubeSandbox 或 E2B 接入 WeKnora。Docker、CubeSandbox、E2B 都在同一个空间设置页面通过同一套配置与检查接口管理;只有远端后端需要本文所述的集群和模板准备。普通智能体使用者不需要搭建模板,也不应该逐项猜测运行环境。
|
||||
|
||||
## 谁负责什么
|
||||
|
||||
| 角色 | 职责 |
|
||||
| --- | --- |
|
||||
| WeKnora 发布流程 | 维护 `docker/Dockerfile.sandbox`,发布与 WeKnora 版本匹配的 `wechatopenai/weknora-sandbox` 镜像 |
|
||||
| 集群管理员 | 部署 CubeSandbox 或开通 E2B,并保证控制面模板 API 可用 |
|
||||
| 空间管理员 | 在“设置 → 沙箱后端”中填写集群地址和凭据,先完成连接验证,再选择接口返回的模板 |
|
||||
| 智能体管理员 | 在智能体的 Skills 配置中选择已经验证过的沙箱后端 |
|
||||
|
||||
“WeKnora 标准模板”指模板内容由 WeKnora 维护,并不代表所有集群共享同一个模板 ID。CubeSandbox 的模板 ID、E2B 的模板 ID/别名都属于具体集群或账号;跨集群硬编码一个 ID 会指向不存在或内容不一致的模板。
|
||||
|
||||
## 标准模板包含什么
|
||||
|
||||
标准镜像定义在 `docker/Dockerfile.sandbox`,当前包含:
|
||||
|
||||
- Python 3.11;
|
||||
- Node.js 20、npm 与 npx;
|
||||
- jq 及基础 Shell 工具;
|
||||
- `/workspace` 工作目录;
|
||||
- UID 1000 的非 root `user` 账号(E2B 模板约定的账号名,WeKnora 以它执行脚本与文件操作)。
|
||||
|
||||
生产环境应使用与 WeKnora 相同的版本标签,不建议长期指向 `latest`。Skills 新增系统依赖时,应先更新标准镜像并重新注册模板,再切换集群的默认模板 ID。
|
||||
|
||||
### 两个镜像变体
|
||||
|
||||
`docker/Dockerfile.sandbox` 产出两个 target,内容相同、入口不同:
|
||||
|
||||
| 变体 | 标签 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `sandbox`(默认) | `wechatopenai/weknora-sandbox:<版本>` | Docker 后端的会话容器镜像;同时作为 E2B 模板的基础镜像 |
|
||||
| `cube` | `wechatopenai/weknora-sandbox:<版本>-cube` | CubeSandbox 模板 |
|
||||
|
||||
区别在于 Cube 变体额外注入了 envd。Cube 直接把 OCI 镜像变成模板,并以 `GET :49983/health` 探活,这个端点只有 envd 提供;不带 envd 的镜像建模板必然以 `connection refused` 失败。E2B 不需要这个变体,因为它的构建流程会自行注入 envd;Docker 后端则完全不需要 envd。详见 [Cube 自带镜像接入](https://cubesandbox.com/zh/guide/tutorials/bring-your-own-image.html)。
|
||||
|
||||
Cube 变体只发布 linux/amd64——envd 的来源镜像 `cubesandbox-base` 没有 arm64,Cube 自身的 PVM 形态也只支持 x86_64。变体内 envd 以 root 运行,脚本仍按请求指定的账号执行,落在同一个 uid 1000 的 `user` 上。
|
||||
|
||||
## CubeSandbox
|
||||
|
||||
### 先选部署形态
|
||||
|
||||
| 形态 | 适用 | 硬性前提 |
|
||||
| --- | --- | --- |
|
||||
| 裸金属 / 物理机 | 已有可用 KVM 的机器 | `/dev/kvm` 可读写;root 权限 |
|
||||
| PVM | 云厂商屏蔽了嵌套虚拟化、`/dev/kvm` 不可用 | 仅 x86_64;需安装 PVM 宿主机内核并重启;ARM64 不支持 |
|
||||
| Kubernetes(preview) | 已有集群、要多节点 | K8s 1.24+;计算节点打标签;计算节点非裸金属时需开启 PVM bootstrap |
|
||||
|
||||
三种形态共同的前提:
|
||||
|
||||
- glibc ≥ 2.31(官方二进制基于 Ubuntu 20.04 构建);
|
||||
- `/data/cubelet` 挂载 XFS,且开启 reflink(快照的 Copy-on-Write 依赖它)。Ubuntu/Debian 默认 ext4,需要单独准备分区或 loop 设备;
|
||||
- 至少 50 GB 可用磁盘(要做多个模板则建议 200 GB 以上)、内存 ≥ 8 GB;
|
||||
- 内核支持 eBPF 且 `/sys/fs/bpf` 已挂载为 bpffs(Cubelet 的网络运行时依赖);
|
||||
- 具备 `resolvectl` 或 NetworkManager(安装脚本据此配置 `cube.app` 的 DNS 解析);
|
||||
- Docker 可用(MySQL/Redis 以 Docker Compose 运行)。
|
||||
|
||||
安装脚本会把 CubeMaster、Cubelet、CubeShim 作为宿主机进程运行,因此**不能在没有 systemd 的容器化环境里部署**。CI 容器、无 init 系统的开发机请改用 K8s 形态或另找一台主机。
|
||||
|
||||
### 制作模板并对接 WeKnora
|
||||
|
||||
1. 按 [CubeSandbox Quick Start](https://github.com/TencentCloud/CubeSandbox/blob/master/docs/zh/guide/quickstart.md) 完成控制面、计算节点、CubeProxy 与域名解析。生产环境还需按官方文档完成鉴权、TLS、网络策略与多节点部署。
|
||||
2. 在 WeKnora 的空间设置中填写 CubeAPI、CubeProxy、sandbox domain 和可选 API Key。需要自定义 guest DNS 时填写「DNS 服务器」(须为 IP);留空则使用集群默认。若这些端点位于 RFC1918/loopback 网络,显式打开“允许访问私网集群地址”。
|
||||
3. 点击“连接并继续”。WeKnora 先验证控制面地址与凭据,通过后才进入模板步骤并列出集群模板。**不会自动创建**。没有 WeKnora 标准模板时在占位卡片上点「创建」,会从 `wechatopenai/weknora-sandbox:main-cube` 发起构建。改 DNS 或需要换镜像时在 weknora 卡片上点「重建」:优先对现有标准模板做 in-place rebuild(模板 ID 不变);只有 redo 被拒绝时才先建新模板、成功后再删旧的。已安装 Skill 的配置(以及同一集群上其它已装 Skill 的配置)不能重建。失败模板同样用「重建」(CubeMaster 拒绝 redo、错误码 130400 时尤其需要)。
|
||||
4. 模板构建状态会自动刷新。状态变为 `READY` 后才可选择并进入运行配置;界面显示模板名称、状态和版本,配置内部才保存该集群自己的 `template_id`。
|
||||
|
||||
模板镜像必须提供 uid 1000 的 `user` 账号:WeKnora 以该账号执行脚本与文件操作。写权限只保证在 `/workspace/output` 与 `/workspace/input` 下。
|
||||
|
||||
多实例 WeKnora 必须配置 Redis,以共享 session 到 sandbox 的绑定。只有单实例开发环境才应使用内存绑定。
|
||||
|
||||
### 验证
|
||||
|
||||
界面上的“连接验证”只覆盖控制面,“完整验证”会真实创建、执行并销毁一个沙箱。要覆盖 WeKnora 实际依赖的全部语义(会话内状态保持、shell_exec 复用同一沙箱、附件暂存、产物收集、执行超时),在能访问集群的机器上跑一致性测试:
|
||||
|
||||
```bash
|
||||
CUBE_API_URL=http://127.0.0.1:33000 \
|
||||
CUBE_PROXY_URL=http://127.0.0.1:80 \
|
||||
CUBE_TEMPLATE_ID=<模板 ID> \
|
||||
go test -tags=integration ./internal/sandbox -run Integration -count=1 -v
|
||||
```
|
||||
|
||||
测试结束会归还自己创建的沙箱;跑完用 `cubemastercli` 确认没有残留实例。
|
||||
|
||||
### 排障
|
||||
|
||||
| 现象 | 排查方向 |
|
||||
| --- | --- |
|
||||
| 连接验证失败 | CubeAPI 地址是否是控制面端口(不是 Dashboard 端口);私网地址是否已打开“允许访问私网集群地址” |
|
||||
| 连接通过但执行报数据面错误 | CubeProxy 地址与 sandbox domain 是否与集群 `CUBE_API_SANDBOX_DOMAIN` 一致;Proxy 是否对 WeKnora 可达 |
|
||||
| 模板长期停在构建中 | 镜像体积与网络;用 `cubemastercli tpl watch --job-id <id>` 看真实进度 |
|
||||
| 模板构建失败 | 卡片上会显示集群返回的失败原因。未安装 Skill 时可点「重建」删掉失败模板并用当前配置新建;仅刷新列表不会再自动重建。已安装 Skill 的配置会拒绝重建,请新建沙箱 |
|
||||
| 失败原因含 `TOOMANYREQUESTS` / Docker Hub 限流 | Cube 节点匿名拉 `wechatopenai/weknora-sandbox` 触发了 Hub 限额。等限额恢复后再刷新,或在节点上 `docker login` 后重试 |
|
||||
| 失败原因是 `Get "http://<IP>:49983/health": connect: connection refused` | 模板镜像里没有 envd。确认建模板用的是 `-cube` 变体镜像;若已是该变体,再查 Cube 沙箱网段(默认 `192.168.0.0/18`)是否与物理内网冲突 |
|
||||
| 沙箱能执行但「出网可用」失败 | 先看模板「公网访问」是否开启;WeKnora 标准模板构建会带 `allowInternetAccess`。guest DNS 来自标准模板构建时写入的 `dns`(设置页「DNS 服务器」/`dns_servers`)。留空时 Cubelet 会落到 `119.29.29.29`;私网或云上 UDP 53 到不了公网 DNS 时,填 Cube 宿主机 `/etc/resolv.conf` 里能用的地址,并避开 CubeVS 默认拒绝的 `10/8`、`172.16/12`、`192.168/16`。未安装 Skill 时,已有标准模板改 DNS 后在 weknora 卡片上点「重建」;已装 Skill 的配置请新建沙箱。其次 Cube 把沙箱 80/443 TPROXY 到 `192.168.0.1:8080/8443`,宿主机占用 `0.0.0.0:8080` 时 cube-egress 起不来。探测目标 `1.1.1.1` 在腾讯云也经常不通,国内目标可达即可 |
|
||||
| 会话重连后状态丢失 | 多副本部署是否配置了 Redis 绑定存储;沙箱是否已被空闲 TTL 回收 |
|
||||
|
||||
### 已知限制
|
||||
|
||||
- K8s 部署仍是 preview:计算节点资源紧张时 Pod 可能被误驱逐,计算面升级会重建 Big Pod 并中断存量沙箱。
|
||||
- 官方镜像的 Multi-Arch 覆盖尚不完整,ARM64 环境需自行构建模板镜像。
|
||||
- 暂不支持 GPU 直通。
|
||||
|
||||
其它 E2B 兼容后端(含容器隔离的 Kubernetes 实现)与协议层的统一方案见 [沙箱协议接入说明](./sandbox-protocol.md)。
|
||||
|
||||
## E2B 及其它 E2B 兼容实现
|
||||
|
||||
E2B 官方托管服务、自建 E2B Infrastructure,以及任意实现 E2B 协议的控制面(例如 Kubernetes 上以容器隔离的 Agent-Sandbox)都通过同一个 E2B 配置接入;自建集群通常还要填写 `proxy_url` 数据面网关,详见 [沙箱协议接入说明](./sandbox-protocol.md)。填写 API Key 后先执行“连接并继续”,流程与 Cube 相同:验证连接后列出账号可见模板;缺少 `weknora` 时点「创建 WeKnora 标准模板」,通过 E2B Template API 从标准镜像启动后台构建。自建部署还需填写 API URL 和 sandbox domain;E2B 上游通过 Terraform 提供 AWS、GCP 等部署方式,具体以 [E2B self-hosting guide](https://github.com/e2b-dev/infra/blob/main/self-host.md) 和 [E2B Template 文档](https://e2b.dev/docs/template/quickstart) 为准。
|
||||
|
||||
## 在设置页面完成接入
|
||||
|
||||
1. 打开“设置 → 沙箱后端”,点击“添加沙箱后端”。
|
||||
2. 填写该集群自己的 API、Proxy、sandbox domain 和凭据;这些值只保存在当前空间配置中,不读取 Sandbox 环境变量。
|
||||
3. 点击“连接并继续”,验证控制面地址和凭据;连接通过后才加载模板列表。
|
||||
4. 没有标准模板时点占位卡片上的「创建」,或选择集群已有的兼容模板;改 DNS / 镜像后在 weknora 卡片上点「重建」。**该配置已安装 Skill 时不能更换或重建模板**——技能环境绑在快照上,新底模不会进去;请新建一份沙箱再装 Skill。构建中的模板不可选择,状态会自动刷新。
|
||||
5. 配置运行参数;上线前可执行一次“完整验证”。完整验证会真实创建、执行并销毁一个沙箱。
|
||||
6. 保存后,在智能体 Skills 配置中选择该后端。对配置的修改只影响之后新建的沙箱;已有会话仍固定使用创建时的配置。
|
||||
|
||||
## 上线检查
|
||||
|
||||
- 模板版本与 WeKnora 版本匹配,且不依赖浮动的 `latest`;
|
||||
- WeKnora 到控制面、Proxy 和 sandbox domain 的 DNS、TLS 与防火墙均已打通;
|
||||
- 多实例部署已配置 Redis;
|
||||
- API Key 只通过密钥管理或加密配置保存;
|
||||
- “连接验证”和“完整验证”均通过;
|
||||
- 已配置运行中与 paused 沙箱的容量监控、费用监控和孤儿清理;
|
||||
- 切换模板或集群前已确认旧会话和旧沙箱的回收策略。
|
||||
@@ -0,0 +1,225 @@
|
||||
# Docker 沙箱后端
|
||||
|
||||
面向部署方与要改这块代码的人。本文说明 docker 后端现在是什么形态、怎么配、边界在哪,
|
||||
以及为什么是这个设计。协议层的整体立场见 [沙箱协议接入说明](./sandbox-protocol.md),
|
||||
CubeSandbox / E2B 的集群与模板见 [沙箱集群与标准模板](./sandbox-cluster.md)。
|
||||
|
||||
## 结论先说
|
||||
|
||||
- docker 后端已经是**会话级后端**:一个会话一个长驻容器,脚本、`shell_exec`、附件暂存、
|
||||
产物收集都落在同一个容器里,与 E2B/Cube 在应用层的行为一致。
|
||||
- 实现方式是 `RemoteSandboxClient` 适配器(`internal/sandbox/docker_remote_client.go`),
|
||||
直接打 Docker Engine API。session→sandbox 绑定、生命周期锁、能力矩阵一行没改——
|
||||
这正是当初把 provider 抽象成 `RemoteSandboxClient` 的收益。
|
||||
- 它适合单机 / 私有化部署。跨主机调度、内核级隔离、内存态快照仍然要用 E2B 协议后端,
|
||||
原因见「边界」一节。
|
||||
- **默认关闭。** 本机 `docker.sock` 等同宿主机 root。系统管理员可在「设置 → 系统设置 → 网络安全」打开,立即生效;也可用环境变量 `WEKNORA_SANDBOX_DOCKER_ENABLED=true` 作为未落库时的回退。设置页始终保留 Docker 标签:未打开时没有「添加」入口,只说明如何启用。已有配置仍可查看/删除,但不会再创建容器。
|
||||
- Docker 官方的 Docker Sandboxes(`sbx`)不能当后端:那是开发者本机 CLI,要 Docker 账号登录、
|
||||
工作区是宿主机目录直挂、没有多租户服务端 API。
|
||||
|
||||
## 之前为什么不行
|
||||
|
||||
旧实现每次执行都是 `docker run --rm` 加一个只读 bind mount,和 E2B 不是「能力少一点」,
|
||||
而是模型不同。具体是这几条(都在改造中修掉了):
|
||||
|
||||
| 旧行为 | 后果 |
|
||||
| --- | --- |
|
||||
| 执行完即销毁容器 | 没有会话状态,`shell_exec`、附件暂存、产物收集在能力矩阵里根本不注册 |
|
||||
| 超时 `kill` 的是 `docker run` 客户端进程 | 容器继续跑到自己结束;WeKnora 已经给用户返回超时了。实测见 [PoC](./poc/docker-sandbox) |
|
||||
| `/workspace` 只读挂载 | 脚本写不了 `/workspace/output`,而 skills 框架恰恰把 `WEKNORA_SKILL_OUTPUT_DIR` 指到那里 |
|
||||
| bind mount 由宿主机 daemon 解释 | WeKnora 自己跑在容器里时挂进去的是宿主机上的同名目录,通常不存在 |
|
||||
| 走 docker CLI 而不是 API | 依赖宿主机装 CLI,错误只能靠字符串匹配,拿不到容器 ID 做对账与回收 |
|
||||
| 配置面只有一个 `image` | CPU、内存、网络、TTL 都没法按空间配置 |
|
||||
|
||||
## 现在的形态
|
||||
|
||||
一个沙箱就是一个容器。标准镜像以 `USER user` 结尾,创建时覆盖为 uid 0,这样入口才能在 `/var/lib` 写下活跃标记;脚本仍以 `user` 执行。PID 1 是 `sleep infinity`,所有工作都通过 exec 进去做。
|
||||
|
||||
这层 wrapper 是通过 `Entrypoint` 下发并把 `Cmd` 显式清空的:daemon 会把镜像自带的
|
||||
ENTRYPOINT 拼到 Cmd 前面,所以只设 Cmd 时,任何声明了 ENTRYPOINT 的镜像(本文件的
|
||||
`cube` target 就有)都会顶掉 PID 1,活跃标记不会生成、exec 也会打到别的东西上。
|
||||
|
||||
| 契约方法 | Docker 实现 |
|
||||
| --- | --- |
|
||||
| `Health` | `GET /_ping` |
|
||||
| `Create` | `POST /containers/create` + `/start`,metadata 落成 labels,镜像缺失时先 pull |
|
||||
| `Connect` | `GET /containers/{id}/json`;容器被停掉时重新 `start`,被 pause 时 `unpause` |
|
||||
| `Get` / `List` | `GET /containers/json?filters=label=…`,服务端按 label 过滤 |
|
||||
| `Delete` | `DELETE /containers/{id}?force=1&v=1` |
|
||||
| `Exec` | `POST /containers/{id}/exec` → `/exec/{id}/start`(hijack)→ `/exec/{id}/json` |
|
||||
| `WriteFile` / `ReadFile` / `Stat` | 刻意不用 archive 接口,走 exec 的 `cat > "$1"` / `cat` / `find -maxdepth 0 -printf`(见下) |
|
||||
| `MakeDir` / `Remove` / `ListDir` | 没有原生接口,用 exec 的 `mkdir -p` / `rm -rf` / `find -printf` |
|
||||
|
||||
几个不显然但要紧的决定:
|
||||
|
||||
**超时由容器内的 `timeout(1)` 执行。** 取消 HTTP 请求不会终止容器里的进程([PoC](./poc/docker-sandbox)
|
||||
里专门复现了这条),所以每次 exec 都包一层
|
||||
`sh -c 'touch <marker>; exec timeout -s KILL <n> "$@"' weknora-exec <cmd> <args...>`。
|
||||
命令通过位置参数传进去,不做任何字符串拼接,脚本里的引号和换行不会改变实际执行的东西。
|
||||
退出码 137/124 被翻译成 `Killed=true`。
|
||||
|
||||
**空闲回收是 WeKnora 自己的事。** daemon 没有任何 TTL 概念。上面那层 wrapper 顺手 `touch`
|
||||
一个活跃标记文件,清扫时用一次 `HEAD /archive` 读它的 mtime 就知道容器多久没干活了——
|
||||
不需要额外往容器里 exec,也不需要 Redis 记账。清扫在 `Create`/`Connect` 时触发,
|
||||
按 daemon 端点限流(默认最快一分钟一次),在后台跑。删掉一个空闲容器不需要跟绑定存储协调:
|
||||
生命周期本来就把「provider 上已经没有的沙箱」当作可重新绑定,这跟 E2B 沙箱被自己的 TTL
|
||||
回收后的路径完全一样。每个容器把创建时的 TTL 记在 label 上,所以 A 配置触发的清扫不会拿
|
||||
自己的 TTL 去衡量 B 配置的容器。
|
||||
|
||||
标记文件必须对沙箱账号可写(只跑脚本的会话也要能刷新它),因此它的 mtime 是容器可影响的:
|
||||
落在未来的时间戳一律不采信,退回按容器启动时间判断,否则一次 `touch -d 2099-01-01`
|
||||
就能让容器永久免于回收。往前改只会让自己更早被回收,不构成问题。此外清扫在真正删除前
|
||||
会再读一次标记:列举加逐个 stat 在繁忙 daemon 上不是瞬时的,期间会话可能已被恢复。
|
||||
|
||||
残留风险:标记对沙箱账号可写是刻意的(只跑脚本的会话也要能刷新它),所以任何能在容器里
|
||||
执行命令的东西——包括普通技能脚本,不需要 root——都可以起一个后台循环持续 `touch` 标记,
|
||||
把自己维持成「一直活跃」。当前没有硬寿命上限,需要的话应由部署方在 daemon 侧限制。
|
||||
|
||||
**所有 exec 都以 `user`(uid 1000) 运行,没有例外。** 脚本执行、`shell_exec`、全部文件操作、
|
||||
以及 manager 自己的产物目录 bootstrap 都跑在沙箱账号下;`RemoteExecRequest.User` 留空时
|
||||
适配器解析成 `DefaultSandboxExecUser` 而不是 root,漏传账号只会失去权限、不会拿到权限。
|
||||
|
||||
bootstrap 尤其不能以 root 跑:产物目录位于会话自己可写的 `/workspace` 下,而 `chown`/`chmod`
|
||||
会跟随符号链接。会话只要把产物目录换成指向 `/etc` 的链接,一次 root bootstrap 就会把 `/etc`
|
||||
的属主交给沙箱账号,接着改写 `passwd` 即可让该账号在下一次 exec 时变成 uid 0(真机验证过)。
|
||||
以沙箱账号执行时这条链直接断在内核:`chown` 对不属于自己的目标一律失败。
|
||||
|
||||
容器 `CapDrop: ALL` 之后额外补回 CHOWN/DAC_OVERRIDE/FOWNER/FSETID/SETGID/SETUID/KILL,
|
||||
Docker 默认给的 NET_RAW、MKNOD、SYS_CHROOT 等一律不给。注意这批 capability 是给容器内
|
||||
**root** 用的(装包、修属主),而目前没有任何 exec 以 root 运行,因此它们对现有路径是冗余的;
|
||||
保留是为了自定义镜像里用 `sudo` 装包的场景,收紧它们是可以独立推进的加固项。
|
||||
|
||||
**文件操作走 exec,不走 archive 接口。** archive 接口(`PUT`/`GET`/`HEAD /archive`)由 daemon
|
||||
执行,这意味着两件事同时成立:它忽略 exec user 一律以 root 操作,并且会在路径解析时跟随符号
|
||||
链接。沙箱账号对 `/workspace` 有写权限,而调用侧的路径守卫都是字符串前缀比较,于是
|
||||
`ln -s /root /workspace/output/esc` 之后,`/workspace/output/esc/secret.txt` 既能通过守卫,
|
||||
又会被 daemon 以 root 读出来(真机验证过,不是推演)。改成以沙箱账号 exec 之后,能不能读写
|
||||
由内核判定,符号链接指向哪里都不再重要,也不存在「先校验后使用」之间被换掉链接的窗口。
|
||||
`Stat` 用 `find`,它不跟随**最后一段**路径,因此路径本身是链接时会如实报告为 `other` 类型,
|
||||
要求正规文件的调用方在尝试读取之前就会拒绝。
|
||||
|
||||
这个保证到最后一段为止:中间层的链接由内核在路径解析时展开,`/workspace/output/链接/passwd`
|
||||
仍会 stat 成普通文件(真机验证过)。因此「只读产物目录」是一个约定而非权限边界——绕过它读到的
|
||||
东西,沙箱账号本来就能用 `shell_exec` 读到,真正的边界始终是内核的权限检查。
|
||||
|
||||
archive 接口里只剩 `HEAD` 还在用,且仅用于读固定路径的活跃标记。
|
||||
|
||||
**PID 1 开 tini(`HostConfig.Init`)。** 容器入口是 `sleep`,它从不调用 `wait()`。长会话里
|
||||
后台进程一旦活得比启动它的 exec 久,退出后就会变成没人回收的僵尸,堆到 `pids_limit` 之后所有
|
||||
后续 exec 都会失败。
|
||||
|
||||
**镜像即模板。** `ListTemplates` 列出 daemon 上带 `com.weknora.sandbox.template=true` 标签的、
|
||||
或名字就是标准镜像的镜像;`EnsureStandardTemplate` 在后台拉取,拉取期间模板状态显示为
|
||||
`building`,与其它后端的模板构建流程对齐。
|
||||
|
||||
## 配置
|
||||
|
||||
在「设置 → 沙箱后端」中新建配置并选择 Docker:
|
||||
|
||||
系统管理员可在「设置 → 系统设置 → 网络安全」打开 Docker 沙箱(立即生效)。未落库时回退到 `WEKNORA_SANDBOX_DOCKER_ENABLED`。默认关闭,因为能保存 Docker 配置的空间管理员可以在本进程够得到的 Engine API 上创建容器,而本机 `docker.sock` 等同宿主机 root。设置页始终有 Docker 标签;未打开时没有添加按钮,只提示如何启用。
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| 镜像 | 必填。会话容器都从它创建,等价于其它后端的 template ID |
|
||||
| Docker 守护进程地址 | 留空跟随本机 `docker` CLI(`DOCKER_HOST` 或当前 `docker context`),因此 Colima / Docker Desktop 不必手填 socket。远程填 `tcp://host:2376`,**必须**同时填 TLS 证书目录;私网地址要打开「允许访问私网集群地址」 |
|
||||
| TLS 证书目录 | 远程 daemon 必填。WeKnora 主机上包含 `ca.pem`/`cert.pem`/`key.pem` 的目录,证书不入库 |
|
||||
| 空闲回收 | 容器多久没执行任何命令就回收。留空 1800 秒 |
|
||||
| CPU / 内存 / 进程数上限 | 单个沙箱的资源上限。留空 2 核 / 2048 MB / 512 进程 |
|
||||
| 网络模式 | 只接受 `bridge`(默认)与 `none`(完全禁止出网)。`host`、`container:` 以及自定义网络名一律拒绝:常见部署通过挂载的 `docker.sock` 连 daemon,填上部署自身的 compose 网络就会让沙箱与 Postgres / Redis 同网 |
|
||||
|
||||
`tcp://` daemon 的连接与其它后端的租户端点同一口径:保存时校验地址,实际拨号时再按
|
||||
「允许访问私网地址」开关过一遍 `SafeDialControl`,这样保存校验解析到公网、连接时被
|
||||
重解析到 169.254.169.254 的情况也拦得住。unix socket 不经过这一层。
|
||||
|
||||
镜像要求:uid 1000 的 `user` 账号、`/workspace/{input,output}` 归该账号所有、
|
||||
GNU `find`(`-printf`)与 coreutils `timeout`。`docker/Dockerfile.sandbox` 产出的标准镜像满足这些,
|
||||
Debian 系基础镜像天然带 find 和 timeout。
|
||||
|
||||
部署形态:
|
||||
|
||||
| 形态 | 适用 | 关键约束 |
|
||||
| --- | --- | --- |
|
||||
| 本机 socket | 单机、私有化、开发 | app 与 daemon 同机;`docker.sock` 等于宿主机 root,只能暴露给 app 进程 |
|
||||
| 远程 daemon(mTLS) | 沙箱负载与应用分离 | 必须配 TLS 证书;daemon 端口不得暴露到公网 |
|
||||
| 每租户独立 daemon | 有强隔离诉求但没有 KVM | 由部署方分配,不同配置指向不同 host |
|
||||
|
||||
WeKnora 自己跑在容器里时,要把 **实际的** docker socket 挂进 app 容器(并接受它等同宿主机 root 的事实),
|
||||
或者改用远程 daemon。Linux 上通常是 `/var/run/docker.sock`;macOS 上 Colima / Docker Desktop / OrbStack
|
||||
各自有 `$HOME` 下的 socket,以 `docker context show` 为准。入口脚本在 `gosu` 降权前会按
|
||||
socket 的 GID 把 `appuser` 加入对应组;不要依赖 compose `group_add`,也不要 `chmod 666`
|
||||
宿主机 socket。若 socket 是 `root:root` 且仅所有者可写,容器内无法安全补权,需在宿主机把
|
||||
socket 改成非 root 组的 `660`。
|
||||
|
||||
## 边界
|
||||
|
||||
这些是 Docker 给不了的,写在这里以免被当成 bug:
|
||||
|
||||
- **跨主机调度**:一个配置就是一个 daemon,也就是一台机器。要多机就得自己选机、分发镜像、
|
||||
在绑定里记住会话落在哪台机器上——那就是在写一个控制面,这条边界是刻意保留的。
|
||||
- **内核级隔离**:容器共享宿主机内核。要更强只能叠 gVisor / Kata(配置里的 runtime 字段)。
|
||||
- **内存态快照**:`docker commit` 只保存文件系统。CRIU(`docker checkpoint`)是 experimental,
|
||||
默认 daemon 直接拒绝。E2B 的 pause/snapshot 会保存内存,Docker 不会。
|
||||
- **域名级出网策略**:Docker 只有 L3/L4。`RemoteNetworkPolicy` 里的域名 allow/deny 在这个后端
|
||||
只能表达成「全开」或「全关」,要按域名放行得在部署侧加 egress proxy。
|
||||
- **卷挂载**:`SupportsVolumes` 目前是 false,租户级共享卷还没有映射到 Docker named volume。
|
||||
|
||||
## 快照
|
||||
|
||||
「空间级管理沙箱装 skill → commit 成快照 → 会话从快照起容器 → 增量出下一版」这套流程已经接入:
|
||||
`DockerRemoteClient` 实现 `RemoteSnapshotManager`,`docker commit` 打出带
|
||||
`com.weknora.sandbox.skill-snapshot` 标签的本地镜像(命名空间 `weknora-skill/`),
|
||||
会话启动时用该镜像覆盖配置里的基础 image。安装器用 root `shell_exec` 写
|
||||
`/opt/weknora/tenant/skills`,与 Cube / E2B 同一条技能安装链路。
|
||||
|
||||
两个要注意的约束:镜像层上限 127,长期增量要定期压平;快照是本机资产,多机部署必须推到 registry。
|
||||
压平和跨 daemon 分发还不在这条路径里。
|
||||
|
||||
磁盘占用的实际形态和 Cube / E2B 不一样,值得单独说清楚:
|
||||
|
||||
- 第 N+1 代是从第 N 代起的容器 commit 出来的,**N 的层完整包含在 N+1 里**。所以两代镜像并存
|
||||
时,旧的那个 tag 不额外占盘;反过来说,`PruneSupersededSnapshots` 到期删掉旧 tag 也几乎
|
||||
回收不了空间。真正回收发生在整条链的 tag 全部退役之后,因此 `DeleteSnapshot` 必须带
|
||||
`PruneChildren`(即 `noprune=0`),否则无 tag 的祖先层会永久留下。
|
||||
- **卸载一个 skill 会让镜像变大**:`rm -rf` 在 overlay 上是新增一层 whiteout,被删的文件仍
|
||||
留在父层。也就是说装和卸都只增不减,Cube / E2B 是每代一张独立模板、到期真删,只有 Docker
|
||||
是单调累积。要把空间还回来,只能从基础模板重建整条链——`SkillSnapshotTriggerRebuild` 为此
|
||||
预留了,但重建流程尚未实现。
|
||||
- 快照的 owner fingerprint 只在 host 是**显式配置**时才把 host 计入。留空的配置一律记成
|
||||
`local-daemon`,不能采用 `DetectLocalDockerHost()` 的结果:那个值来自 `DOCKER_HOST` 或当前
|
||||
docker context,切一次 Colima / Docker Desktop / OrbStack 就会变,而它一变就等价于「凭据
|
||||
轮换」——会话静默退回基础模板(skill 全部消失)、安装被拒、快照清理被永久跳过。本机 daemon
|
||||
换个 host 字符串通常还是同一块盘上的同一批镜像,跨账号那套推理在这里不成立。
|
||||
|
||||
回收路径依赖 ledger 能给每张快照命名,而 `snapshot_id` 只有在 provider 应答之后才写得下来。
|
||||
进程死在 commit 与那次写入之间,就会留下一张谁都叫不出名字的快照:`PruneSupersededSnapshots`
|
||||
因为状态是 `building` 而跳过它,`ReconcileSnapshots` 只告警不删,配置删除时空 `snapshot_id`
|
||||
被当成「无需释放」。因此 `planned_name` 在 commit **之前**就落库(迁移 000088),之后靠
|
||||
provider 的 `ListSnapshots` 按名字认领:Cube / E2B 会把请求的名字回显在 `Names` 里,Docker 的
|
||||
ID 本身就是这个名字加上 `weknora-skill/` 前缀。两条路径会用它——周期清理里的
|
||||
`reapAbandonedBuilds`,以及配置删除时的 `resolveAbandonedBuildIDs`(配置一删,周期清理就再也
|
||||
遍历不到这张快照,那是最后一次机会)。只有**认领成功**才会删;名字对不上时不动,因为无法区分
|
||||
「commit 从未发生」和「该 provider 不回显名字」,猜错就等于丢掉一张仍然存在的快照的唯一记录。
|
||||
|
||||
## 测试
|
||||
|
||||
单元测试用一个内存版 Engine API 驱动适配器,不需要 daemon:
|
||||
|
||||
```bash
|
||||
go test ./internal/sandbox -run 'TestDocker' -count=1
|
||||
```
|
||||
|
||||
一致性测试打真实 daemon,覆盖会话状态保持、包安装跨执行存活、`shell_exec` 复用同一沙箱、
|
||||
附件暂存与产物收集、超时确实终止进程、容器被外部停掉后恢复:
|
||||
|
||||
```bash
|
||||
docker build -f docker/Dockerfile.sandbox --target sandbox -t wechatopenai/weknora-sandbox:dev .
|
||||
DOCKER_INTEGRATION_IMAGE=wechatopenai/weknora-sandbox:dev \
|
||||
go test -tags=docker_integration ./internal/sandbox \
|
||||
-run '^TestDocker.*Integration' -count=1 -v -timeout=15m
|
||||
```
|
||||
|
||||
它和 E2B 的一致性测试断言的是同一批语义,这是刻意重复:一个后端只过其中一个,
|
||||
就说明两者在应用层还不能互换。
|
||||
|
||||
[docs/poc/docker-sandbox](./poc/docker-sandbox) 是当初的可行性验证,保留下来作为
|
||||
「Docker 能做什么、不能做什么」的可复现证据,它不参与主模块构建。
|
||||
@@ -0,0 +1,103 @@
|
||||
# WeKnora 沙箱:以 E2B 协议为唯一接入契约
|
||||
|
||||
本文说明 WeKnora 为什么把 E2B 协议当作沙箱后端的唯一对接契约、现在还有哪些例外、以及可以直接拿来用的开源实现有哪些。面向部署方与要新接一种沙箱后端的开发者。
|
||||
|
||||
## 结论
|
||||
|
||||
- 跨主机、需要内核级隔离的部署走 E2B 协议(控制面 REST + 数据面 envd):WeKnora 只维护一套这样的客户端,具体隔离能力由社区实现提供,见下面的选型表。
|
||||
- 单机 / 私有化部署可以直接用 `docker` 后端:它现在也是会话级后端(一个会话一个长驻容器),在应用层与 E2B 行为一致,代价是空闲回收、执行超时这些控制面职责由 WeKnora 承担。详见 [Docker 沙箱后端](./sandbox-docker-backend.md)。
|
||||
- `local` 后端在 WeKnora 主机上直接跑脚本,没有任何隔离,只适合可信的开发空间。
|
||||
|
||||
## 当前的后端形态
|
||||
|
||||
| 后端 | 协议 | 会话内状态 | shell_exec / 附件暂存 / 产物收集 | 定位 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `e2b` | E2B 协议 | 持久(一个会话一个沙箱) | 支持 | 生产主路径,可指向任意 E2B 兼容控制面 |
|
||||
| `cube` | E2B 兼容(走 Cube 官方 Go SDK) | 持久 | 支持 | CubeSandbox 专用适配器,见“为什么还留着 cube 适配器” |
|
||||
| `docker` | 无(Docker Engine API) | 持久(一个会话一个容器) | 支持 | 单机 / 私有化部署,见 [Docker 沙箱后端](./sandbox-docker-backend.md) |
|
||||
| `local` | 无(本机进程) | 无 | 不支持 | 本机开发调试,隔离性最弱 |
|
||||
|
||||
`docker` 与 E2B 协议后端的分界不在能力而在规模:一个 docker 配置就是一台 daemon,跨主机调度、内核级隔离、内存态快照都不在它的能力范围内,那些正是 E2B 兼容实现提供的东西。`local` 依然不参与会话级能力集。能力矩阵在 `internal/sandbox/capabilities.go` 中显式表达,agent 侧据此决定是否注册 shell/文件类工具。
|
||||
|
||||
## 可直接使用的开源实现
|
||||
|
||||
| 实现 | 隔离方式 | 部署前提 | 适用场景 |
|
||||
| --- | --- | --- | --- |
|
||||
| [CubeSandbox](https://github.com/TencentCloud/CubeSandbox)(Apache-2.0) | KVM MicroVM,eBPF 网络隔离 | 裸金属/物理机需 `/dev/kvm`;普通云主机可用 PVM 内核;`/data/cubelet` 需 XFS(reflink);K8s 部署为 preview | 需要内核级隔离、高密度、快照/回滚 |
|
||||
| [Agent-Sandbox](https://github.com/agent-sandbox/agent-sandbox)(Apache-2.0) | Kubernetes Pod(容器),可叠加 gVisor/Kata runtimeClass | 一个 K8s 集群(1.26+),`kubectl apply -f install.yaml` | 已有 K8s、想要“容器版 E2B”、不想引入虚拟化依赖 |
|
||||
| [e2b-dev/infra](https://github.com/e2b-dev/infra)(Apache-2.0) | Firecracker MicroVM | Nomad/Consul + 云厂商 Terraform(AWS/GCP) | 想自建与 E2B Cloud 完全一致的栈 |
|
||||
| [E2B Cloud](https://e2b.dev) | 托管 MicroVM | 只需 API Key | 不想自己运维 |
|
||||
|
||||
选型要点:
|
||||
|
||||
- 只有容器可用(没有 KVM、也不想上 PVM 内核)时,走 Agent-Sandbox 这类 K8s 原生实现,而不是给 WeKnora 加一个 Docker 控制面。
|
||||
- 单机、有 KVM 或可装 PVM 内核,走 CubeSandbox。
|
||||
- 上述实现都通过同一个 `e2b` 配置接入,WeKnora 侧零改动。
|
||||
|
||||
不建议采用的方向:`e2bgateway`、`circlesac/sandbox`、`Cage` 这类项目虽然也宣称 E2B 兼容并支持 Docker 后端,但当前 star 数与维护强度都在个位数量级,作为生产依赖风险过高。
|
||||
|
||||
## 怎么接入一个 E2B 兼容控制面
|
||||
|
||||
在“设置 → 沙箱后端”中新建配置,选择 `E2B`,填写:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `api_key` | 控制面凭据。自建集群通常是它自己签发的 token |
|
||||
| `api_url` | 控制面地址,例如 `http://agent-sandbox.internal/e2b/v1`。留空则用 E2B Cloud |
|
||||
| `sandbox_domain` | 沙箱域名。数据面地址形如 `49983-<sandboxID>.<sandbox_domain>` |
|
||||
| `proxy_url` | 数据面网关地址。见下 |
|
||||
| `template_id` | 模板 / 镜像标识 |
|
||||
| 允许访问私网集群地址 | 集群位于 RFC1918/loopback 时必须打开 |
|
||||
|
||||
`proxy_url` 是自建集群的关键:E2B Cloud 通过公网 DNS 解析每个沙箱的域名并提供证书,自建集群通常把所有沙箱收敛到一个网关地址、按 Host 头路由。填了 `proxy_url` 之后,WeKnora 会把数据面请求直接拨到该网关,同时保留沙箱域名在 Host 头里;网关是 `http://` 时还会把数据面 scheme 一并降级——E2B SDK 把它写死成 https,这一步省掉了为泛域名申请证书的成本。控制面请求不受影响,仍走共享连接池(实现见 `internal/sandbox/gateway_transport.go`)。
|
||||
|
||||
配置保存前先执行“连接并继续”,上线前执行一次“完整验证”,后者会真实创建、执行并销毁一个沙箱。
|
||||
|
||||
## envd 协议的兼容性坑
|
||||
|
||||
数据面 envd 的契约和 `github.com/matiasinsaurralde/go-e2b` 的实现之间有两处偏差,WeKnora 在 `internal/sandbox/envd_compat_transport.go` 里统一补齐:
|
||||
|
||||
- 认证:envd 要求 `Authorization: Basic base64("<user>:")`,SDK 发的是 `X-User-ID` 头。E2B Cloud 对此宽容,其他实现直接返回 `unauthenticated: no user specified`。
|
||||
- 文件上传:envd 的 `POST /files` 只接受 `multipart/form-data`,SDK 发的是裸 `application/octet-stream`,会得到 500。
|
||||
|
||||
另外健康探针改用 `GET /v2/sandboxes`:旧的 `GET /sandboxes` 已不在客户端其他调用路径上,部分 E2B 兼容实现也只实现了 v2,用旧接口探活会把健康的后端判成不可用。文件操作现在也显式声明执行账号(`user`),与脚本运行账号保持一致,而不是依赖各实现的默认值。
|
||||
|
||||
模板镜像需要提供 `user` 账号(uid 1000),这是 E2B 模板的既定约定;WeKnora 以该账号执行脚本与文件操作。写权限只保证在 `/workspace/output`(产物目录,执行前由 WeKnora 创建并授权)与 `/workspace/input`(附件暂存)下,脚本不应假设 `/workspace` 根目录可写。
|
||||
|
||||
## 一致性测试
|
||||
|
||||
`internal/sandbox/e2b_compatible_integration_test.go` 是面向任意 E2B 兼容控制面的一致性测试,覆盖会话内状态保持、shell_exec 复用同一沙箱、附件暂存、产物收集、执行超时。接一种新后端时先跑它:
|
||||
|
||||
```bash
|
||||
E2B_INTEGRATION_API_URL=http://127.0.0.1:18080/e2b/v1 \
|
||||
E2B_INTEGRATION_API_KEY=<token> \
|
||||
E2B_INTEGRATION_TEMPLATE=code-interpreter \
|
||||
E2B_INTEGRATION_SANDBOX_DOMAIN=localhost \
|
||||
E2B_INTEGRATION_PROXY_URL=http://127.0.0.1:18080 \
|
||||
go test -tags=e2b_integration ./internal/sandbox \
|
||||
-run '^TestE2BCompatibleControlPlaneConformance' -count=1 -v -timeout=15m
|
||||
```
|
||||
|
||||
针对 E2B Cloud 时不要设置 `E2B_INTEGRATION_PROXY_URL`。该套件已在 Kubernetes 上的 Agent-Sandbox(容器后端)实测通过。
|
||||
|
||||
### 在本机复现一个容器版 E2B 后端
|
||||
|
||||
只需要 Docker,用 kind 起一个单节点集群即可,全程不涉及 KVM:
|
||||
|
||||
```bash
|
||||
kind create cluster --name e2b-poc
|
||||
kubectl create namespace agent-sandbox
|
||||
kubectl apply -n agent-sandbox -f https://raw.githubusercontent.com/agent-sandbox/agent-sandbox/main/install.yaml
|
||||
|
||||
# 控制面需要一份模板配置;集群里没有 gVisor 时,先把模板的 runtimeClassName 去掉
|
||||
kubectl -n agent-sandbox create configmap agent-sandbox \
|
||||
--from-file=sandbox.yaml --from-file=templates.json
|
||||
|
||||
kubectl -n agent-sandbox port-forward svc/agent-sandbox 18080:80
|
||||
```
|
||||
|
||||
之后把 `api_url` 指向 `http://127.0.0.1:18080/e2b/v1`、`proxy_url` 指向 `http://127.0.0.1:18080`、`sandbox_domain` 填 `localhost`,即可用上面的命令跑一致性测试。默认 token 在 install.yaml 中,生产部署务必替换。
|
||||
|
||||
## 为什么还留着 cube 适配器
|
||||
|
||||
CubeSandbox 兼容 E2B SDK,理论上可以只用 `e2b` 配置接入。目前仍保留独立适配器,原因是它使用 Cube 官方 Go SDK,模板构建、网络策略等控制面能力与 Cube 的 API 一一对应,而这些在通用 E2B 客户端里还没有等价物。合并的前置条件是:在真实 Cube 集群上跑通上面的一致性测试,并把模板构建、网络策略两块能力对齐到通用客户端。数据面路由已经不再是障碍——`proxy_url` 已经泛化成所有远端后端共用的能力。
|
||||
@@ -0,0 +1,153 @@
|
||||
package docs_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
docs "github.com/Tencent/WeKnora/docs"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type swaggerDocumentCase struct {
|
||||
name string
|
||||
loadSpec func(t *testing.T) []byte
|
||||
parse func([]byte, any) error
|
||||
}
|
||||
|
||||
func swaggerDocuments() []swaggerDocumentCase {
|
||||
return []swaggerDocumentCase{
|
||||
{
|
||||
name: "registered document",
|
||||
loadSpec: func(t *testing.T) []byte {
|
||||
t.Helper()
|
||||
return []byte(docs.SwaggerInfo.ReadDoc())
|
||||
},
|
||||
parse: json.Unmarshal,
|
||||
},
|
||||
{
|
||||
name: "swagger.json",
|
||||
loadSpec: func(t *testing.T) []byte {
|
||||
t.Helper()
|
||||
return readSwaggerFile(t, "swagger.json")
|
||||
},
|
||||
parse: json.Unmarshal,
|
||||
},
|
||||
{
|
||||
name: "swagger.yaml",
|
||||
loadSpec: func(t *testing.T) []byte {
|
||||
t.Helper()
|
||||
return readSwaggerFile(t, "swagger.yaml")
|
||||
},
|
||||
parse: yaml.Unmarshal,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestKnowledgeSearchRouteContract(t *testing.T) {
|
||||
for _, tt := range swaggerDocuments() {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assertKnowledgeSearchRouteContract(t, tt.loadSpec(t), tt.parse)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestModelDeleteUsageContract(t *testing.T) {
|
||||
for _, tt := range swaggerDocuments() {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assertModelDeleteUsageContract(t, tt.loadSpec(t), tt.parse)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func readSwaggerFile(t *testing.T, name string) []byte {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(name)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", name, err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func assertKnowledgeSearchRouteContract(t *testing.T, data []byte, parse func([]byte, any) error) {
|
||||
t.Helper()
|
||||
var spec struct {
|
||||
Paths map[string]map[string]any `json:"paths" yaml:"paths"`
|
||||
}
|
||||
if err := parse(data, &spec); err != nil {
|
||||
t.Fatalf("parse generated Swagger document: %v", err)
|
||||
}
|
||||
|
||||
knowledgeSearch, ok := spec.Paths["/knowledge-search"]
|
||||
if !ok {
|
||||
t.Fatal("generated Swagger document does not expose /knowledge-search")
|
||||
}
|
||||
if _, ok := knowledgeSearch["post"]; !ok {
|
||||
t.Fatal("generated Swagger document does not expose POST /knowledge-search")
|
||||
}
|
||||
|
||||
if staleRoute, ok := spec.Paths["/sessions/search"]; ok {
|
||||
if _, ok := staleRoute["post"]; ok {
|
||||
t.Fatal("generated Swagger document still exposes stale POST /sessions/search")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertModelDeleteUsageContract(t *testing.T, data []byte, parse func([]byte, any) error) {
|
||||
t.Helper()
|
||||
var spec struct {
|
||||
Paths map[string]map[string]struct {
|
||||
Responses map[string]any `json:"responses" yaml:"responses"`
|
||||
} `json:"paths" yaml:"paths"`
|
||||
Definitions map[string]map[string]any `json:"definitions" yaml:"definitions"`
|
||||
}
|
||||
if err := parse(data, &spec); err != nil {
|
||||
t.Fatalf("parse generated Swagger document: %v", err)
|
||||
}
|
||||
|
||||
models, ok := spec.Paths["/models/{id}"]
|
||||
if !ok {
|
||||
t.Fatal("generated Swagger document does not expose /models/{id}")
|
||||
}
|
||||
deleteOperation, ok := models["delete"]
|
||||
if !ok {
|
||||
t.Fatal("generated Swagger document does not expose DELETE /models/{id}")
|
||||
}
|
||||
if _, ok := deleteOperation.Responses["400"]; !ok {
|
||||
t.Fatal("model DELETE Swagger contract does not document the model-in-use 400 response")
|
||||
}
|
||||
|
||||
const errorCodeDefinition = "github_com_Tencent_WeKnora_internal_errors.ErrorCode"
|
||||
errorCodes, ok := spec.Definitions[errorCodeDefinition]
|
||||
if !ok {
|
||||
t.Fatalf("generated Swagger document does not expose %s", errorCodeDefinition)
|
||||
}
|
||||
enum, ok := errorCodes["enum"].([]any)
|
||||
if !ok {
|
||||
t.Fatalf("generated Swagger ErrorCode enum has unexpected shape: %#v", errorCodes["enum"])
|
||||
}
|
||||
enumVarNames, ok := errorCodes["x-enum-varnames"].([]any)
|
||||
if !ok {
|
||||
t.Fatalf("generated Swagger ErrorCode names have unexpected shape: %#v", errorCodes["x-enum-varnames"])
|
||||
}
|
||||
for i, value := range enum {
|
||||
if swaggerInteger(value) == 2300 {
|
||||
if i >= len(enumVarNames) || enumVarNames[i] != "ErrModelInUse" {
|
||||
t.Fatalf("error code 2300 must align with ErrModelInUse, got names=%v", enumVarNames)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatal("generated Swagger ErrorCode enum does not include model-in-use code 2300")
|
||||
}
|
||||
|
||||
func swaggerInteger(value any) int {
|
||||
switch number := value.(type) {
|
||||
case int:
|
||||
return number
|
||||
case float64:
|
||||
return int(number)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
title: API文档概览
|
||||
tags: [API参考, REST, 认证, 接口]
|
||||
aliases: [API概览, API文档, API参考]
|
||||
source: api/README.md
|
||||
---
|
||||
|
||||
# API 文档概览
|
||||
|
||||
WeKnora 提供了一系列 RESTful API,用于创建和管理知识库、检索知识,以及进行基于知识的问答。
|
||||
|
||||
## 基础信息
|
||||
|
||||
- **基础 URL**: `/api/v1`
|
||||
- **响应格式**: JSON
|
||||
- **认证方式**: API Key
|
||||
|
||||
> API 认证使用 WeKnora 本地 JWT,OIDC 认证流程参见 [OIDC认证调用流程](../安全认证/OIDC认证调用流程.md)
|
||||
|
||||
## 认证机制
|
||||
|
||||
所有 API 请求需要在 HTTP 请求头中包含 `X-API-Key`:
|
||||
|
||||
```
|
||||
X-API-Key: your_api_key
|
||||
X-Request-ID: unique_request_id # 建议,便于追踪
|
||||
```
|
||||
|
||||
API Key 在 Web 页面完成账户注册后,前往账户信息页面获取。
|
||||
|
||||
## 错误处理
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "错误代码",
|
||||
"message": "错误信息",
|
||||
"details": "错误详情"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API 分类
|
||||
|
||||
| 分类 | 描述 | 详细文档 |
|
||||
|------|------|----------|
|
||||
| 认证管理 | 用户注册、登录、令牌管理;OIDC 流程 | [auth.md](../../api/auth.md) · [OIDC认证调用流程.md](../安全认证/OIDC认证调用流程.md) |
|
||||
| 空间管理 | 创建和管理空间账户 | [tenant.md](../../api/tenant.md) |
|
||||
| 知识库管理 | 创建、查询和管理知识库 | [knowledge-base.md](../../api/knowledge-base.md) |
|
||||
| 知识管理 | 上传、检索和管理知识内容 | [knowledge.md](../../api/knowledge.md) |
|
||||
| 模型管理 | 配置和管理各种AI模型 | [model.md](../../api/model.md) |
|
||||
| 分块管理 | 管理知识的分块内容 | [chunk.md](../../api/chunk.md) |
|
||||
| 标签管理 | 管理知识库的标签分类 | [tag.md](../../api/tag.md) |
|
||||
| FAQ管理 | 管理FAQ问答对 | [faq.md](../../api/faq.md) |
|
||||
| 智能体管理 | 创建和管理自定义智能体 | [agent.md](../../api/agent.md) |
|
||||
| 会话管理 | 创建和管理对话会话 | [session.md](../../api/session.md) |
|
||||
| 知识搜索 | 在知识库中搜索内容 | [knowledge-search.md](../../api/knowledge-search.md) |
|
||||
| 聊天功能 | 基于知识库和 Agent 进行问答 | [chat.md](../../api/chat.md) |
|
||||
| 消息管理 | 获取和管理对话消息 | [message.md](../../api/message.md) |
|
||||
| 评估功能 | 评估模型性能 | [evaluation.md](../../api/evaluation.md) |
|
||||
| 初始化管理 | 知识库模型配置与 Ollama 管理 | [initialization.md](../../api/initialization.md) |
|
||||
| 系统管理 | 系统信息、解析引擎、存储引擎 | [system.md](../../api/system.md) |
|
||||
| MCP 服务 | MCP 工具服务管理 | [mcp-service.md](../../api/mcp-service.md) |
|
||||
| 组织管理 | 组织、成员、知识库/智能体共享 | [organization.md](../../api/organization.md) |
|
||||
| Skills | 预装智能体技能 | [skill.md](../../api/skill.md) |
|
||||
| 网络搜索 | 网络搜索服务商 | [web-search.md](../../api/web-search.md) |
|
||||
| 向量存储 | 向量数据库连接管理 | [vector-store.md](../../api/vector-store.md) |
|
||||
|
||||
> 各 API 的详细说明参见 `docs/api/` 目录下的对应文档
|
||||
|
||||
## 相关主题
|
||||
|
||||
- [OIDC认证调用流程](../安全认证/OIDC认证调用流程.md) — API 认证的 OIDC 流程
|
||||
- [内置模型管理](../核心功能/内置模型管理.md) — 模型管理 API 的配置参考
|
||||
- [MCP功能使用说明](../核心功能/MCP功能使用说明.md) — MCP 服务管理 API 的使用
|
||||
- [共享空间说明](../安全认证/共享空间说明.md) — 组织管理 API 的业务逻辑
|
||||
- [IM集成开发](../集成扩展/IM集成开发.md) — IM 渠道管理 API
|
||||
- [数据源导入开发](../集成扩展/数据源导入开发.md) — 数据源管理 API
|
||||
|
||||
---
|
||||
|
||||
## 反向链接
|
||||
|
||||
- [Home](../Home.md) — Wiki 首页导航
|
||||
- [OIDC认证调用流程](../安全认证/OIDC认证调用流程.md) — API 认证机制与 OIDC 相关
|
||||
- [内置模型管理](../核心功能/内置模型管理.md) — 模型管理 API 的底层配置
|
||||
- [MCP功能使用说明](../核心功能/MCP功能使用说明.md) — MCP 服务 API 的使用场景
|
||||
- [共享空间说明](../安全认证/共享空间说明.md) — 组织管理 API 的业务逻辑
|
||||
- [IM集成开发](../集成扩展/IM集成开发.md) — IM 渠道 API 的使用场景
|
||||
- [数据源导入开发](../集成扩展/数据源导入开发.md) — 数据源 API 的使用场景
|
||||
@@ -0,0 +1,127 @@
|
||||
---
|
||||
title: WeKnora Wiki 首页
|
||||
tags: [首页, 导航, MOC]
|
||||
aliases: [Home, Index, wiki首页]
|
||||
---
|
||||
|
||||
# WeKnora Wiki
|
||||
|
||||
欢迎使用 WeKnora 知识库 wiki!这里是 WeKnora 项目文档的互联知识网络,所有页面通过双向链接关联,帮助你从任意入口探索整个知识体系。
|
||||
|
||||
---
|
||||
|
||||
## 项目概述
|
||||
|
||||
| 页面 | 简介 |
|
||||
|------|------|
|
||||
| [版本路线图](项目概述/版本路线图.md) | 产品规划与计划方向 |
|
||||
| [Lite与标准版区别](项目概述/Lite与标准版区别.md) | 轻量版与标准版的功能对比 |
|
||||
|
||||
## 核心功能
|
||||
|
||||
| 页面 | 简介 |
|
||||
|------|------|
|
||||
| [知识图谱](核心功能/知识图谱.md) | Neo4j 知识图谱的快速开始与使用 |
|
||||
| [开启知识图谱功能](核心功能/开启知识图谱功能.md) | 知识图谱功能的完整启用流程 |
|
||||
| [MCP功能使用说明](核心功能/MCP功能使用说明.md) | MCP 服务的用户操作指南 |
|
||||
| [内置MCP服务管理](核心功能/内置MCP服务管理.md) | 内置 MCP 服务的系统级管理 |
|
||||
| [内置模型管理](核心功能/内置模型管理.md) | 内置模型的系统级管理 |
|
||||
| [Agent技能系统](核心功能/Agent技能系统.md) | Agent Skills 扩展机制与预加载技能 |
|
||||
|
||||
## 集成与扩展
|
||||
|
||||
| 页面 | 简介 |
|
||||
|------|------|
|
||||
| [IM集成开发](集成扩展/IM集成开发.md) | 企业即时通讯平台接入开发 |
|
||||
| [数据源导入开发](集成扩展/数据源导入开发.md) | 外部平台数据自动同步与导入 |
|
||||
| [添加网络搜索引擎](集成扩展/添加网络搜索引擎.md) | 扩展新的网络搜索 Provider |
|
||||
| [集成向量数据库](集成扩展/集成向量数据库.md) | 集成新的向量数据库检索引擎 |
|
||||
|
||||
## 安全与认证
|
||||
|
||||
| 页面 | 简介 |
|
||||
|------|------|
|
||||
| [OIDC认证调用流程](安全认证/OIDC认证调用流程.md) | OIDC 第三方登录的完整调用链路 |
|
||||
| [空间RBAC说明](安全认证/RBAC说明.md) | 空间内角色矩阵、资源归属与审计 |
|
||||
| [共享空间说明](安全认证/共享空间说明.md) | 跨空间协作与知识库/智能体共享 |
|
||||
|
||||
## 开发与部署
|
||||
|
||||
| 页面 | 简介 |
|
||||
|------|------|
|
||||
| [开发指南](开发部署/开发指南.md) | 本地开发环境搭建与工作流 |
|
||||
| [快速开发模式](开发部署/快速开发模式.md) | 后端/前端热更新开发模式 |
|
||||
|
||||
## 运维与排障
|
||||
|
||||
| 页面 | 简介 |
|
||||
|------|------|
|
||||
| [常见问题](运维排障/常见问题.md) | 部署与使用中的 FAQ |
|
||||
|
||||
## API 参考
|
||||
|
||||
| 页面 | 简介 |
|
||||
|------|------|
|
||||
| [API文档概览](API参考/API文档概览.md) | RESTful API 基础信息与分类索引 |
|
||||
|
||||
---
|
||||
|
||||
## 知识图谱
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
Home[WeKnora Wiki]
|
||||
Home --> 项目概述
|
||||
Home --> 核心功能
|
||||
Home --> 集成扩展
|
||||
Home --> 安全认证
|
||||
Home --> 开发部署
|
||||
Home --> 运维排障
|
||||
Home --> API参考
|
||||
|
||||
项目概述 --> ROADMAP[版本路线图]
|
||||
项目概述 --> LITE[Lite与标准版区别]
|
||||
|
||||
核心功能 --> KG[知识图谱]
|
||||
核心功能 --> KGSetup[开启知识图谱功能]
|
||||
核心功能 --> MCP[MCP功能使用说明]
|
||||
核心功能 --> BuiltinMCP[内置MCP服务管理]
|
||||
核心功能 --> BuiltinModel[内置模型管理]
|
||||
核心功能 --> Skills[Agent技能系统]
|
||||
|
||||
集成扩展 --> IM[IM集成开发]
|
||||
集成扩展 --> DS[数据源导入开发]
|
||||
集成扩展 --> WebSearch[添加网络搜索引擎]
|
||||
集成扩展 --> VecDB[集成向量数据库]
|
||||
|
||||
安全认证 --> OIDC[OIDC认证调用流程]
|
||||
安全认证 --> RBAC[空间RBAC说明]
|
||||
安全认证 --> SharedSpace[共享空间说明]
|
||||
|
||||
开发部署 --> DevGuide[开发指南]
|
||||
开发部署 --> QuickDev[快速开发模式]
|
||||
|
||||
运维排障 --> FAQ[常见问题]
|
||||
|
||||
API参考 --> APIOverview[API文档概览]
|
||||
|
||||
KG -.-> KGSetup
|
||||
MCP -.-> BuiltinMCP
|
||||
BuiltinMCP -.-> BuiltinModel
|
||||
Skills -.-> IM
|
||||
IM -.-> DS
|
||||
DS -.-> SharedSpace
|
||||
OIDC -.-> RBAC
|
||||
RBAC -.-> SharedSpace
|
||||
OIDC -.-> SharedSpace
|
||||
LITE -.-> SharedSpace
|
||||
WebSearch -.-> VecDB
|
||||
DevGuide -.-> QuickDev
|
||||
FAQ -.-> DevGuide
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 反向链接
|
||||
|
||||
本页为 wiki 首页,所有页面均链接回此处。
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
title: OIDC认证调用流程
|
||||
tags: [安全认证, OIDC, 认证, 登录, SSO]
|
||||
aliases: [OIDC, OIDC认证, SSO登录, 第三方登录]
|
||||
source: OIDC认证调用流程.md
|
||||
---
|
||||
|
||||
# OIDC 认证调用流程
|
||||
|
||||
本文档说明 WeKnora 当前 OIDC 登录能力的实际调用过程,覆盖前后端完整链路。
|
||||
|
||||
> OIDC 认证是标准版多空间场景下的登录方式,[Lite 版](../项目概述/Lite与标准版区别.md)不需要
|
||||
|
||||
## 整体设计说明
|
||||
|
||||
本项目的 OIDC 登录采用 **后端发起授权参数生成、后端接收回调并完成 code 换 token、前端通过 URL hash 接收最终登录结果** 的模式。
|
||||
|
||||
核心特点:
|
||||
|
||||
1. **前端只负责发起跳转**,不直接和 OIDC Provider 交换 token
|
||||
2. **后端负责用授权码 `code` 向 OIDC Provider 换取 token**
|
||||
3. 后端拿到 OIDC 用户信息后,会查找本地用户;若不存在则自动创建本地账号和默认空间
|
||||
4. 最终签发 WeKnora 自己的本地 JWT,OIDC token 只用于后端换取用户身份
|
||||
|
||||
> 自动创建用户与空间的逻辑与 [共享空间说明](../安全认证/共享空间说明.md) 的多空间模型相关
|
||||
|
||||
## 相关接口
|
||||
|
||||
| 接口 | 说明 |
|
||||
|------|------|
|
||||
| `GET /api/v1/auth/oidc/config` | 获取 OIDC 是否启用及 Provider 展示名称 |
|
||||
| `GET /api/v1/auth/oidc/url` | 生成第三方登录跳转地址 |
|
||||
| `GET /api/v1/auth/oidc/callback` | OIDC Provider 回调地址 |
|
||||
|
||||
## 调用流程(4 个阶段)
|
||||
|
||||
### 阶段一:前端发现能力
|
||||
|
||||
前端调用 `/auth/oidc/config`,决定是否展示第三方登录入口。
|
||||
|
||||
### 阶段二:浏览器跳转授权
|
||||
|
||||
前端调用 `/auth/oidc/url` 获取授权地址,然后跳转到 OIDC Provider。
|
||||
|
||||
### 阶段三:后端完成身份兑换
|
||||
|
||||
Provider 回调后端 `/auth/oidc/callback`,后端用 `code` 换 token、拉取用户信息、关联或创建本地用户,并签发 WeKnora JWT。
|
||||
|
||||
### 阶段四:前端接收最终结果
|
||||
|
||||
后端 302 回前端,通过 `#oidc_result` 传递登录结果;前端在 `App.vue` 中统一解析。
|
||||
|
||||
## 关键配置项
|
||||
|
||||
| 配置项 | 说明 |
|
||||
|--------|------|
|
||||
| `OIDC_AUTH_ENABLE` | 是否启用 OIDC 登录 |
|
||||
| `OIDC_AUTH_CLIENT_ID` | OIDC Client ID |
|
||||
| `OIDC_AUTH_CLIENT_SECRET` | OIDC Client Secret |
|
||||
| `OIDC_AUTH_DISCOVERY_URL` | OIDC Discovery 地址 |
|
||||
| `OIDC_AUTH_SCOPES` | Scope 列表,默认 `openid profile email` |
|
||||
|
||||
启用时的最小要求:`client_id` + `client_secret` + (`discovery_url` 或 `authorization_endpoint + token_endpoint`)
|
||||
|
||||
## 本地联调示例(Dex)
|
||||
|
||||
项目中已提供 Dex 示例配置:`misc/dex-config.yaml`。
|
||||
|
||||
> 除 Dex 外,也可以使用 KeyCloak 等其他符合 OpenID Connect 协议的 Provider
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **`redirect_uri` 必须严格匹配** Provider 客户端配置
|
||||
2. **邮箱是本地账号关联主键** — 若 Provider 没返回 email,将无法完成登录
|
||||
3. **首次 OIDC 登录会自动创建用户和默认空间**
|
||||
4. **真正用于访问 API 的是本地 JWT**,不是 OIDC access token
|
||||
|
||||
## 相关主题
|
||||
|
||||
- [共享空间说明](../安全认证/共享空间说明.md) — 多空间场景下的用户与组织管理
|
||||
- [Lite与标准版区别](../项目概述/Lite与标准版区别.md) — Lite 版无需 OIDC(单空间)
|
||||
- [API文档概览](../API参考/API文档概览.md) — API 认证机制
|
||||
|
||||
---
|
||||
|
||||
## 反向链接
|
||||
|
||||
- [Home](../Home.md) — Wiki 首页导航
|
||||
- [共享空间说明](../安全认证/共享空间说明.md) — OIDC 创建的用户与空间可用于共享空间
|
||||
- [Lite与标准版区别](../项目概述/Lite与标准版区别.md) — Lite 不需要 OIDC(单空间无需注册)
|
||||
- [API文档概览](../API参考/API文档概览.md) — API 的认证机制与 OIDC JWT 相关
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
title: 空间RBAC说明
|
||||
tags: [安全认证, RBAC, 权限, 多空间, 角色]
|
||||
aliases: [RBAC, 角色权限, 空间角色, TenantRBAC]
|
||||
source: RBAC说明.md
|
||||
---
|
||||
|
||||
# 空间 RBAC 说明
|
||||
|
||||
本文档介绍 WeKnora 的**空间内权限控制(Workspace RBAC)**,包括角色矩阵、资源归属模型,以及它与 [共享空间](./共享空间说明.md) 的关系。
|
||||
|
||||
> 状态:已发布;由配置项 `tenant.enable_rbac` 控制,默认 `true`(强制鉴权)。
|
||||
> 完整说明、灰度方案、Schema、路由守卫等参见 [`docs/RBAC说明.md`](../../RBAC说明.md)。
|
||||
|
||||
## 解决的问题
|
||||
|
||||
RBAC 引入前,只要通过 `X-API-Key` 或 JWT 认证成功,调用方在空间内基本等同管理员。一旦一个空间出现多名真人成员,就需要区分:
|
||||
|
||||
- 谁可以删除知识库、撤销 API Key;
|
||||
- 谁可以编辑「自己」的 KB / Agent;
|
||||
- 谁只读。
|
||||
|
||||
## 角色矩阵
|
||||
|
||||
| 角色 | 标识 | 关键能力 |
|
||||
|------|------|----------|
|
||||
| 只读 | `viewer` | 仅读 |
|
||||
| 贡献者 | `contributor` | 可变更 `creator_id == 自己` 的资源;他人资源按 Viewer |
|
||||
| 管理员 | `admin` | 可变更空间内任意资源;管理成员、共享基础设施 |
|
||||
| Owner | `owner` | Admin + 可删空间;每个空间至少一位,可以有多位 |
|
||||
|
||||
层级 `viewer < contributor < admin < owner`,高角色继承低角色。
|
||||
|
||||
Owner 的数量约束是“至少一位”,而不是“只能一位”。系统允许同一空间存在多个活跃 Owner;降级或移除 Owner 时,只有会导致空间失去最后一位 Owner 的操作才会被拒绝。
|
||||
|
||||
### 鉴权层的例外
|
||||
|
||||
- **跨空间超管**:`enable_cross_tenant_access` 打开且账号 `CanAccessAllTenants=true`,通过 `X-Tenant-ID` 切换后等同 Admin。
|
||||
- **API Key**:合成虚拟用户在所属空间内固定 Admin(删空间除外)。
|
||||
- **孤儿空间自愈**:首位认证的真人自动晋升 Owner,避免 API Key-only 空间锁死。
|
||||
|
||||
## 资源归属
|
||||
|
||||
迁移 `000043` 在关键表加上 `creator_id`:
|
||||
|
||||
- `knowledge_bases.creator_id` —— 老数据回填为该空间的 Owner;
|
||||
- `custom_agents.creator_id` + `runnable_by_viewer`(默认 `true`,允许 Viewer 在对话中调用)。
|
||||
|
||||
子资源沿 `chunk → knowledge → kb → creator_id` 链回溯。
|
||||
|
||||
由此得到两类守卫:
|
||||
|
||||
- **角色守卫**:只看角色,用于空间级基础设施(模型、向量库、IM 通道等)。
|
||||
- **归属守卫**`OwnedXxxOrAdmin`:creator 或 Admin+ 二者其一即放行,用于具体资源写操作。
|
||||
|
||||
## 与共享空间的关系
|
||||
|
||||
| 维度 | 解决什么 | 主键 |
|
||||
|------|---------|------|
|
||||
| **空间 RBAC** | 同一空间内「你能对自己/别人/共享基础设施做什么」 | `tenant_members(user_id, tenant_id, role)` |
|
||||
| **共享空间** | 跨空间「让别的空间的人也能用我的 KB / Agent」 | `organization_members` + 共享关系 |
|
||||
|
||||
两者**正交**:
|
||||
|
||||
- 共享空间不持有 KB / Agent,只记录「以何种权限共享到了哪个空间」;资源归属与 `creator_id` 不变;
|
||||
- 一次对**他人共享过来**的 KB 的写操作,需要同时满足:共享时设了「可写」 + 你在该空间不是 Viewer + 源空间的 RBAC 仍然放行;
|
||||
- API Key 跨空间访问**不会**带 Admin 光环——共享路径由 `organization_members.role` 决定,与 API Key 无关。
|
||||
|
||||
判定顺序见 `internal/middleware/kb_access.go`:
|
||||
|
||||
```text
|
||||
KB 属于我当前空间?─是─► 走空间 RBAC(角色 + creator_id)
|
||||
└─否─► KB 共享给我所在空间?─是─► 取 min(共享权限, 空间角色)
|
||||
└─否─► 403 / 404
|
||||
```
|
||||
|
||||
一句话:**空间 RBAC 是纵向的纵深防御,共享空间是横向的协作通道;跨空间写动作必须同时穿过两道闸口。**
|
||||
|
||||
## 配置
|
||||
|
||||
```yaml
|
||||
tenant:
|
||||
enable_rbac: true # false 则进入「仅记录不拦截」灰度窗口
|
||||
enable_cross_tenant_access: false
|
||||
auth:
|
||||
registration_mode: self_serve # 或 invite_only
|
||||
audit:
|
||||
retention_days: 90 # 0 表示不清理
|
||||
```
|
||||
|
||||
环境变量 `WEKNORA_TENANT_ENABLE_RBAC` / `WEKNORA_AUDIT_RETENTION_DAYS` 覆盖 YAML。`DISABLE_REGISTRATION=true` 等价于把 `registration_mode` 强制设为 `invite_only`。
|
||||
|
||||
## 审计
|
||||
|
||||
`audit_logs` 表记录:
|
||||
|
||||
- `rbac.member_added` / `removed` / `role_changed` / `left`
|
||||
- `rbac.access_denied`(仅强制鉴权时;1 分钟滑动窗口去重)
|
||||
|
||||
每日后台 goroutine 清理超过 `audit.retention_days` 的旧行。
|
||||
|
||||
## 相关主题
|
||||
|
||||
- [共享空间说明](./共享空间说明.md) — 跨空间协作与共享,与 RBAC 正交
|
||||
- [OIDC认证调用流程](./OIDC认证调用流程.md) — 多空间用户体系的认证入口
|
||||
- [Lite与标准版区别](../项目概述/Lite与标准版区别.md) — Lite 单用户场景下 RBAC 实际不发挥作用
|
||||
|
||||
---
|
||||
|
||||
## 反向链接
|
||||
|
||||
- [Home](../Home.md) — Wiki 首页导航
|
||||
- [共享空间说明](./共享空间说明.md) — 共享空间访问最终落到空间 RBAC 校验
|
||||
- [OIDC认证调用流程](./OIDC认证调用流程.md) — JWT 解析后即进入 RBAC 角色匹配
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
title: 共享空间说明
|
||||
tags: [安全认证, 共享空间, 协作, 多空间, 权限]
|
||||
aliases: [共享空间, SharedSpace, Organization, 组织]
|
||||
source: 共享空间说明.md
|
||||
---
|
||||
|
||||
# 共享空间说明
|
||||
|
||||
本文档说明 WeKnora 中的**共享空间**功能,包括空间创建与加入、成员角色与权限、知识库与智能体共享规则。
|
||||
|
||||
> 共享空间是标准版功能,[Lite 版](../项目概述/Lite与标准版区别.md)不提供
|
||||
|
||||
## 共享空间概述
|
||||
|
||||
共享空间是跨空间协作的载体。用户可以在同一系统内属于不同空间(账户),通过**加入同一共享空间**,实现:
|
||||
|
||||
- 共享知识库:将本空间的知识库共享到空间,供空间内其他成员使用
|
||||
- 共享智能体:将本空间的智能体共享到空间,供空间内其他成员在对话等场景中使用
|
||||
- 访问他人共享的知识库和智能体
|
||||
|
||||
### 核心概念对照
|
||||
|
||||
| 概念 | 说明 |
|
||||
|------|------|
|
||||
| 共享空间 | 系统中的「组织」(Organization),用于跨空间共享 |
|
||||
| 空间创建者 | 自动成为管理员,不可被移除或降级 |
|
||||
| 空间成员 | 管理员 / 编辑者 / 只读 三种角色 |
|
||||
| 知识库/智能体归属 | 始终属于一个空间;共享不改变归属 |
|
||||
|
||||
## 角色与权限
|
||||
|
||||
| 能力 | 管理员 | 编辑者 | 只读 |
|
||||
|------|:-:|:-:|:-:|
|
||||
| 查看、检索共享知识库 | ✓ | ✓ | ✓ |
|
||||
| 编辑共享知识库内容 | ✓ | ✓ | ✗ |
|
||||
| 将知识库共享到本空间 | ✓ | ✓ | ✗ |
|
||||
| 管理空间设置与成员 | ✓ | ✗ | ✗ |
|
||||
| 生成/刷新邀请码 | ✓ | ✗ | ✗ |
|
||||
|
||||
## 知识库共享规则
|
||||
|
||||
- 只有知识库所属空间下的用户才能发起共享
|
||||
- 发起人必须是目标空间的**管理员**或**编辑者**
|
||||
- 共享时指定权限:只读或可写
|
||||
- 同一知识库可共享到多个空间,各自独立配置权限
|
||||
|
||||
## 智能体共享规则
|
||||
|
||||
- 智能体共享到空间时**仅支持只读**
|
||||
- 同一智能体可共享到多个空间
|
||||
|
||||
> 智能体须已配置完成(如已选模型、若使用知识库则已选 rerank 模型等)方可共享。模型配置参见 [内置模型管理](../核心功能/内置模型管理.md)
|
||||
|
||||
## 智能体停用机制
|
||||
|
||||
- 停用是当前空间对**通过共享空间获得的智能体**的个人偏好设置
|
||||
- 仅影响本空间在对话中选择智能体时的展示,不改变共享关系
|
||||
- 不影响其他成员
|
||||
|
||||
## 相关主题
|
||||
|
||||
- [空间RBAC说明](./RBAC说明.md) — 单空间内的角色与资源归属,跨空间写操作需同时满足两边
|
||||
- [Lite与标准版区别](../项目概述/Lite与标准版区别.md) — Lite 不支持共享空间
|
||||
- [OIDC认证调用流程](../安全认证/OIDC认证调用流程.md) — 多空间场景下的用户认证
|
||||
- [数据源导入开发](../集成扩展/数据源导入开发.md) — 数据源导入的知识库可被共享
|
||||
- [内置模型管理](../核心功能/内置模型管理.md) — 智能体共享前需确保模型已配置
|
||||
|
||||
---
|
||||
|
||||
## 反向链接
|
||||
|
||||
- [Home](../Home.md) — Wiki 首页导航
|
||||
- [空间RBAC说明](./RBAC说明.md) — 共享空间访问最终落到空间 RBAC 校验
|
||||
- [Lite与标准版区别](../项目概述/Lite与标准版区别.md) — Lite 不支持共享空间
|
||||
- [OIDC认证调用流程](../安全认证/OIDC认证调用流程.md) — 多空间用户体系支撑共享空间
|
||||
- [数据源导入开发](../集成扩展/数据源导入开发.md) — 导入的知识库可通过共享空间共享
|
||||
- [内置模型管理](../核心功能/内置模型管理.md) — 智能体共享前的模型配置要求
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
title: 开发指南
|
||||
tags: [开发部署, 开发, 本地开发, 环境搭建]
|
||||
aliases: [开发环境, DevGuide, 开发指南]
|
||||
source: 开发指南.md
|
||||
---
|
||||
|
||||
# 开发指南
|
||||
|
||||
## 快速开发模式(推荐)
|
||||
|
||||
如果需要频繁修改 `app` 或 `frontend` 代码,不需要每次都重新构建 Docker 镜像,可以使用本地开发模式。
|
||||
|
||||
> 详细的开发模式说明参见 [快速开发模式](../开发部署/快速开发模式.md)
|
||||
|
||||
### 方式一:使用 Make 命令(推荐)
|
||||
|
||||
```bash
|
||||
# 终端 1:启动基础设施
|
||||
make dev-start
|
||||
|
||||
# 终端 2:启动后端
|
||||
make dev-app
|
||||
|
||||
# 终端 3:启动前端
|
||||
make dev-frontend
|
||||
```
|
||||
|
||||
### 方式二:使用脚本命令
|
||||
|
||||
```bash
|
||||
./scripts/dev.sh start # 启动基础设施
|
||||
./scripts/dev.sh app # 启动后端
|
||||
./scripts/dev.sh frontend # 启动前端
|
||||
```
|
||||
|
||||
## 访问地址
|
||||
|
||||
| 服务 | 地址 |
|
||||
|------|------|
|
||||
| 前端开发服务器 | http://localhost:5173 |
|
||||
| 后端 API | http://localhost:8080 |
|
||||
| PostgreSQL | localhost:5432 |
|
||||
| Redis | localhost:6379 |
|
||||
| MinIO Console | http://localhost:9001 |
|
||||
| Neo4j Browser | http://localhost:7474 |
|
||||
|
||||
> Neo4j 的使用参见 [知识图谱](../核心功能/知识图谱.md) 和 [开启知识图谱功能](../核心功能/开启知识图谱功能.md)
|
||||
|
||||
## 使用 Air 实现后端热重载
|
||||
|
||||
```bash
|
||||
go install github.com/air-verse/air@latest
|
||||
make dev-app # 检测到 Air 时自动启用热重载
|
||||
```
|
||||
|
||||
## 前端独立开发
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm run dev # 连接到 http://localhost:8080 的后端 API
|
||||
```
|
||||
|
||||
## 生产环境部署
|
||||
|
||||
```bash
|
||||
sh scripts/build_images.sh # 构建所有镜像
|
||||
sh scripts/start_all.sh # 启动生产环境
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
- **启动 dev-app 时报错连接不到数据库**:确保先运行了 `make dev-start`
|
||||
- **前端 CORS 错误**:检查 `vite.config.ts` 中的代理配置
|
||||
- **DocReader 需要重新构建**:`sh scripts/build_images.sh -d`
|
||||
|
||||
> 更多排查建议参见 [常见问题](../运维排障/常见问题.md)
|
||||
|
||||
## 相关主题
|
||||
|
||||
- [快速开发模式](../开发部署/快速开发模式.md) — 开发模式的架构说明
|
||||
- [知识图谱](../核心功能/知识图谱.md) — 开发环境中的 Neo4j 配置
|
||||
- [Agent技能系统](../核心功能/Agent技能系统.md) — 沙箱镜像的构建
|
||||
|
||||
---
|
||||
|
||||
## 反向链接
|
||||
|
||||
- [Home](../Home.md) — Wiki 首页导航
|
||||
- [快速开发模式](../开发部署/快速开发模式.md) — 开发模式的架构对比说明
|
||||
- [知识图谱](../核心功能/知识图谱.md) — 开发环境中的 Neo4j 启动
|
||||
- [开启知识图谱功能](../核心功能/开启知识图谱功能.md) — 开发环境中启用图谱功能
|
||||
- [常见问题](../运维排障/常见问题.md) — 开发相关故障排查
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
title: 快速开发模式
|
||||
tags: [开发部署, 开发, 热更新, Air]
|
||||
aliases: [快速开发, QuickDev, dev模式]
|
||||
source: 快速开发模式说明.md
|
||||
---
|
||||
|
||||
# 快速开发模式
|
||||
|
||||
解决开发流程中,每次修改 `app`(后端)或 `frontend`(前端)代码后,都需要打包 Docker 镜像的问题,实现这两个模块的热更新。
|
||||
|
||||
> 本页是 [开发指南](../开发部署/开发指南.md) 的补充,提供更详细的架构说明
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 方式 1:Make 命令(推荐)
|
||||
|
||||
```bash
|
||||
make dev-start # 终端 1:启动基础设施
|
||||
make dev-app # 终端 2:启动后端
|
||||
make dev-frontend # 终端 3:启动前端
|
||||
```
|
||||
|
||||
### 方式 2:开发脚本
|
||||
|
||||
```bash
|
||||
./scripts/dev.sh start # 终端 1
|
||||
./scripts/dev.sh app # 终端 2
|
||||
./scripts/dev.sh frontend # 终端 3
|
||||
```
|
||||
|
||||
### 方式 3:一键启动
|
||||
|
||||
```bash
|
||||
./scripts/quick-dev.sh
|
||||
```
|
||||
|
||||
### 使用 Air 实现后端热重载
|
||||
|
||||
```bash
|
||||
go install github.com/air-verse/air@latest
|
||||
make dev-app # 自动检测 Air 并使用
|
||||
```
|
||||
|
||||
## 架构对比
|
||||
|
||||
### 开发模式
|
||||
|
||||
```
|
||||
本地后端 App (:8080) ← 本地前端 UI (:5173) → Docker 基础设施容器
|
||||
(PostgreSQL/Redis/MinIO/Neo4j/DocReader)
|
||||
```
|
||||
|
||||
- 后端/前端在本地直接运行,修改后秒级生效
|
||||
- 基础设施仍使用 Docker 容器
|
||||
|
||||
### 生产模式
|
||||
|
||||
```
|
||||
容器后端 App (:8080) ← 容器前端 UI (:80) → 基础设施容器
|
||||
```
|
||||
|
||||
- 所有服务均运行在 Docker 容器中
|
||||
- 每次修改需要重新构建镜像
|
||||
|
||||
## 效率对比
|
||||
|
||||
| 方式 | 每次修改耗时 |
|
||||
|------|------------|
|
||||
| 旧方式(重建镜像) | 2-5 分钟 |
|
||||
| 新方式(本地开发) | 后端 5-10 秒,前端实时热重载 |
|
||||
|
||||
## 相关主题
|
||||
|
||||
- [开发指南](../开发部署/开发指南.md) — 完整的开发环境搭建指南
|
||||
- [常见问题](../运维排障/常见问题.md) — 开发模式相关问题排查
|
||||
|
||||
---
|
||||
|
||||
## 反向链接
|
||||
|
||||
- [Home](../Home.md) — Wiki 首页导航
|
||||
- [开发指南](../开发部署/开发指南.md) — 完整开发指南(本页的补充说明)
|
||||
- [常见问题](../运维排障/常见问题.md) — 开发模式相关故障排查
|
||||
@@ -0,0 +1,104 @@
|
||||
---
|
||||
title: Agent技能系统
|
||||
tags: [核心功能, Agent, Skills, 技能, 沙箱]
|
||||
aliases: [Agent Skills, 技能系统, agent-skills]
|
||||
source: agent-skills.md
|
||||
---
|
||||
|
||||
# Agent 技能系统
|
||||
|
||||
## 概述
|
||||
|
||||
Agent Skills 是一种让 Agent 通过阅读"使用说明书"来学习新能力的扩展机制。与传统的硬编码工具不同,Skills 通过注入到 System Prompt 来扩展 Agent 的能力,遵循 **Progressive Disclosure(渐进式披露)** 的设计理念。目前仅支持带**智能推理**能力的智能体使用。
|
||||
|
||||
### 核心特性
|
||||
|
||||
- **非侵入式扩展**:不影响原有 Agent ReAct 流程
|
||||
- **按需加载**:三级渐进式加载,优化 Token 使用
|
||||
- **沙箱执行**:脚本在隔离环境中安全执行
|
||||
- **灵活配置**:支持多目录、白名单过滤
|
||||
|
||||
> Skills 与 [MCP](../核心功能/MCP功能使用说明.md) 是两种不同的 Agent 扩展机制:Skills 通过 Prompt 注入,MCP 通过协议调用外部工具。
|
||||
|
||||
## 设计理念
|
||||
|
||||
### Progressive Disclosure(渐进式披露)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Level 1: 元数据 (Metadata) │
|
||||
│ • 始终加载到 System Prompt • 约 100 tokens/skill │
|
||||
│ • 包含:技能名称 + 简短描述 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓ 用户请求匹配时
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Level 2: 指令 (Instructions) │
|
||||
│ • 通过 read_skill 工具按需加载 • SKILL.md 的指令内容 │
|
||||
│ • 包含:详细指令、代码示例、使用方法 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓ 需要更多信息时
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Level 3: 附加资源 (Resources) │
|
||||
│ • 通过 read_skill 工具加载特定文件 │
|
||||
│ • 通过 execute_skill_script 执行脚本 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Skill 目录结构
|
||||
|
||||
```
|
||||
my-skill/
|
||||
├── SKILL.md # 必需:主文件(含 YAML frontmatter)
|
||||
├── REFERENCE.md # 可选:补充文档
|
||||
├── templates/ # 可选:模板文件
|
||||
└── scripts/ # 可选:可执行脚本
|
||||
```
|
||||
|
||||
## 预加载技能
|
||||
|
||||
系统内置了以下 5 个预加载技能:
|
||||
|
||||
| 技能 | 用途 |
|
||||
|------|------|
|
||||
| citation-generator | 自动生成规范引用格式 |
|
||||
| data-processor | 数据处理与分析 |
|
||||
| doc-coauthoring | 引导用户完成结构化文档创作 |
|
||||
| document-analyzer | 深度分析文档结构和内容 |
|
||||
| summary-generator | 内容摘要生成 |
|
||||
|
||||
预加载技能位于 `skills/preloaded/` 目录下。
|
||||
|
||||
## 沙箱安全机制
|
||||
|
||||
### 脚本安全校验
|
||||
|
||||
执行前进行多层安全校验:危险命令检测、危险模式匹配、网络访问检测、反向 Shell 检测、参数注入检测等。
|
||||
|
||||
### Sandbox 配置
|
||||
|
||||
空间管理员在「设置 → 沙箱后端」中统一维护 Docker、CubeSandbox 或 E2B 配置。远端模板从目标集群实时获取;缺少 WeKnora 标准模板时由系统自动创建。智能体在编辑页选择一份空间配置,没有选择时不执行技能脚本。Docker、CubeSandbox、E2B 均保留会话级沙箱。
|
||||
|
||||
## 配置示例
|
||||
|
||||
```json
|
||||
{
|
||||
"skills_enabled": true,
|
||||
"skill_dirs": ["/path/to/project/skills"],
|
||||
"allowed_skills": ["pdf-processing", "code-review"]
|
||||
}
|
||||
```
|
||||
|
||||
## 相关主题
|
||||
|
||||
- [MCP功能使用说明](MCP功能使用说明.md) — 另一种 Agent 扩展机制
|
||||
- [IM集成开发](../集成扩展/IM集成开发.md) — Agent 可通过 IM 渠道使用技能
|
||||
- [开发指南](../开发部署/开发指南.md) — 沙箱镜像的构建
|
||||
|
||||
---
|
||||
|
||||
## 反向链接
|
||||
|
||||
- [Home](../Home.md) — Wiki 首页导航
|
||||
- [MCP功能使用说明](MCP功能使用说明.md) — 与 Skills 并列的 Agent 扩展机制
|
||||
- [IM集成开发](../集成扩展/IM集成开发.md) — Agent 在 IM 渠道中可使用技能
|
||||
- [版本路线图](../项目概述/版本路线图.md) — 路线图中的 Skills 社区扩展方向
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
title: MCP功能使用说明
|
||||
tags: [核心功能, MCP, 工具集成]
|
||||
aliases: [MCP使用, MCP功能]
|
||||
source: MCP功能使用说明.md
|
||||
---
|
||||
|
||||
# MCP 功能使用说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
- MCP(Model Context Protocol)让 WeKnora 可以安全地连接外部工具或数据源,扩展 Agent 在推理时可调用的能力。
|
||||
- 在前端 `设置 > MCP 服务`(`frontend/src/views/settings/McpSettings.vue`)中集中管理所有服务,无需手动改配置文件。
|
||||
- 每个服务都包含名称、传输方式(SSE / HTTP Streamable / Stdio)、连接地址或命令、认证信息以及高级超时与重试策略。
|
||||
|
||||
> 关于系统级的内置 MCP 服务管理,参见 [内置MCP服务管理](内置MCP服务管理.md)
|
||||
|
||||
## 入口与界面
|
||||
|
||||
- 打开控制台左侧菜单 `设置 -> MCP 服务`,即可看到当前空间下的所有 MCP 服务列表。
|
||||
- 列表中可快速启停服务、查看描述,并通过右侧菜单执行"测试 / 编辑 / 删除"。
|
||||
- "添加服务"按钮会弹出 `McpServiceDialog`,用于创建或修改服务。
|
||||
|
||||
## 常用操作流程
|
||||
|
||||
### 1. 新建服务
|
||||
|
||||
- 点击"添加服务",填写名称与描述,选择传输方式。
|
||||
- SSE / HTTP Streamable 需提供可访问的服务 URL;Stdio 需配置 `uvx`/`npx` 命令与参数,可附加环境变量。
|
||||
- 根据需要填写 API Key、Bearer Token、超时与重试策略,保存后服务会出现在列表中。
|
||||
|
||||
### 2. 启停服务
|
||||
|
||||
- 在列表开关中切换启用状态,系统会即时调用后端 `updateMCPService`,失败时会自动回滚状态并弹出提示。
|
||||
|
||||
### 3. 连接测试
|
||||
|
||||
- 通过更多菜单选择"测试",前端会调用 `/api/v1/mcp-services/{id}/test` 并弹出 `McpTestResult`。
|
||||
- 成功时会展示服务可用的工具清单(含输入 schema)和资源列表;失败时会显示错误信息,方便排查网络或鉴权问题。
|
||||
|
||||
### 4. 编辑 / 删除
|
||||
|
||||
- "编辑"会带出原有配置,修改后保存即可。
|
||||
- "删除"需要在弹窗中确认,完成后列表自动刷新。
|
||||
|
||||
## 使用建议
|
||||
|
||||
- **传输方式选择**:优先使用 SSE 获取流式体验;需要标准 HTTP Streamable 兼容时再切换;本地调试或离线环境适合使用 Stdio 并在同机启动 MCP Server。
|
||||
- **鉴权管理**:将 API Key / Token 保存在"认证配置"中,生产环境建议单独创建最小权限 Key,并定期轮换。
|
||||
- **重试策略**:对公网或第三方服务适当提高 `retry_count` 与 `retry_delay`,避免间歇性超时导致 Agent 中断
|
||||
|
||||
## 相关主题
|
||||
|
||||
- [内置MCP服务管理](../核心功能/内置MCP服务管理.md) — 系统管理员视角的内置 MCP 服务配置
|
||||
- [Agent技能系统](Agent技能系统.md) — 另一种 Agent 扩展机制
|
||||
- [IM集成开发](../集成扩展/IM集成开发.md) — IM 渠道中 Agent 使用 MCP 工具
|
||||
- [添加网络搜索引擎](../集成扩展/添加网络搜索引擎.md) — 扩展搜索能力的另一种方式
|
||||
|
||||
---
|
||||
|
||||
## 反向链接
|
||||
|
||||
- [Home](../Home.md) — Wiki 首页导航
|
||||
- [内置MCP服务管理](内置MCP服务管理.md) — MCP 的系统级管理(管理员视角)
|
||||
- [Agent技能系统](Agent技能系统.md) — 与 MCP 并列的 Agent 扩展机制
|
||||
- [IM集成开发](../集成扩展/IM集成开发.md) — Agent 在 IM 渠道中可调用 MCP 工具
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
title: 内置MCP服务管理
|
||||
tags: [核心功能, MCP, 系统管理, 内置服务]
|
||||
aliases: [内置MCP, BuiltinMCP, BUILTIN_MCP_SERVICES]
|
||||
source: BUILTIN_MCP_SERVICES.md
|
||||
---
|
||||
|
||||
# 内置 MCP 服务管理指南
|
||||
|
||||
## 概述
|
||||
|
||||
内置 MCP 服务是系统级别的 MCP(Model Context Protocol)服务配置,对所有空间可见,但敏感信息会被隐藏,且不可编辑或删除。内置 MCP 服务通常用于提供系统默认的外部工具和资源接入,确保所有空间都能使用统一的 MCP 服务。
|
||||
|
||||
> 用户视角的 MCP 服务操作参见 [MCP功能使用说明](MCP功能使用说明.md)
|
||||
|
||||
## 内置 MCP 服务特性
|
||||
|
||||
- **所有空间可见**:内置 MCP 服务对所有空间都可见,无需单独配置
|
||||
- **安全保护**:内置 MCP 服务的敏感信息(URL、认证配置、Headers、环境变量)会被隐藏,无法查看详情
|
||||
- **只读保护**:内置 MCP 服务不能被编辑或删除,仅支持测试连接
|
||||
- **统一管理**:由系统管理员统一维护,确保配置一致性和安全性
|
||||
|
||||
## 与内置模型的对比
|
||||
|
||||
| 特性 | 内置模型 | 内置 MCP 服务 |
|
||||
|------|---------|--------------|
|
||||
| 标识字段 | `is_builtin` | `is_builtin` |
|
||||
| 可见范围 | 所有空间 | 所有空间 |
|
||||
| 隐藏信息 | API Key、Base URL | URL、认证配置、Headers、环境变量 |
|
||||
| 编辑保护 | 不可编辑/删除 | 不可编辑/删除 |
|
||||
| 前端标签 | 显示"内置"标签 | 显示"内置"标签 |
|
||||
| 启停控制 | — | 禁用开关(始终启用) |
|
||||
|
||||
> 内置模型的详细管理参见 [内置模型管理](内置模型管理.md)
|
||||
|
||||
## 如何添加内置 MCP 服务
|
||||
|
||||
内置 MCP 服务需要通过数据库直接插入。
|
||||
|
||||
### 1. 准备服务数据
|
||||
|
||||
- 服务名称(name)
|
||||
- 服务描述(description)
|
||||
- 传输方式(transport_type):`sse` 或 `http-streamable`
|
||||
- 服务地址(url)
|
||||
- 认证配置(auth_config)
|
||||
- 高级配置(advanced_config)
|
||||
- 空间ID(tenant_id):建议使用小于 10000 的空间ID
|
||||
|
||||
**支持的传输方式**:
|
||||
- `sse`:Server-Sent Events,推荐用于流式体验
|
||||
- `http-streamable`:HTTP Streamable,标准 HTTP 兼容
|
||||
|
||||
> 注意:出于安全考虑,`stdio` 传输方式在服务端已被禁用。
|
||||
|
||||
### 2. 执行 SQL 插入语句
|
||||
|
||||
```sql
|
||||
INSERT INTO mcp_services (
|
||||
id, tenant_id, name, description, enabled,
|
||||
transport_type, url, auth_config, advanced_config, is_builtin
|
||||
) VALUES (
|
||||
'builtin-mcp-001', 10000, 'Web Search', '内置 Web 搜索 MCP 服务',
|
||||
true, 'sse', 'https://mcp.example.com/sse',
|
||||
'{"api_key": "your-api-key"}'::jsonb,
|
||||
'{"timeout": 30, "retry_count": 3, "retry_delay": 1}'::jsonb,
|
||||
true
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
```
|
||||
|
||||
### 3. 验证插入结果
|
||||
|
||||
```sql
|
||||
SELECT id, name, transport_type, enabled, is_builtin
|
||||
FROM mcp_services WHERE is_builtin = true ORDER BY created_at;
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **ID 命名规范**:建议使用 `builtin-mcp-{序号}` 格式
|
||||
2. **空间ID**:建议使用第一个空间ID(通常是 10000)
|
||||
3. **JSON 格式**:`auth_config`、`advanced_config`、`headers` 必须是有效的 JSON
|
||||
4. **幂等性**:使用 `ON CONFLICT (id) DO NOTHING` 确保重复执行不会报错
|
||||
5. **安全性**:内置 MCP 服务的 URL、认证信息在前端会被自动隐藏
|
||||
6. **传输方式限制**:仅支持 `sse` 和 `http-streamable`
|
||||
|
||||
## 将现有 MCP 服务设置为内置服务
|
||||
|
||||
```sql
|
||||
UPDATE mcp_services SET is_builtin = true WHERE id = '服务ID' AND name = '服务名称';
|
||||
```
|
||||
|
||||
## 移除内置 MCP 服务
|
||||
|
||||
```sql
|
||||
UPDATE mcp_services SET is_builtin = false WHERE id = '服务ID';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 反向链接
|
||||
|
||||
- [Home](../Home.md) — Wiki 首页导航
|
||||
- [MCP功能使用说明](MCP功能使用说明.md) — 用户视角的 MCP 服务操作
|
||||
- [内置模型管理](内置模型管理.md) — 同为内置系统配置,模式相似
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
title: 内置模型管理
|
||||
tags: [核心功能, 模型, 系统管理, 内置服务]
|
||||
aliases: [内置模型, BuiltinModel, BUILTIN_MODELS]
|
||||
source: BUILTIN_MODELS.md
|
||||
---
|
||||
|
||||
# 内置模型管理指南
|
||||
|
||||
## 概述
|
||||
|
||||
内置模型是系统级别的模型配置,对所有空间可见,但敏感信息会被隐藏,且不可编辑或删除。内置模型通常用于提供系统默认的模型配置,确保所有空间都能使用统一的模型服务。
|
||||
|
||||
> 同为内置系统配置的还有 [内置MCP服务管理](内置MCP服务管理.md)
|
||||
|
||||
## 内置模型特性
|
||||
|
||||
- **所有空间可见**:无需单独配置
|
||||
- **安全保护**:API Key、Base URL 被隐藏
|
||||
- **只读保护**:不能被编辑或删除,只能设置为默认模型
|
||||
- **统一管理**:由系统管理员统一维护
|
||||
|
||||
## 如何添加内置模型
|
||||
|
||||
### 1. 准备模型数据
|
||||
|
||||
- 模型名称(name)
|
||||
- 模型类型(type):`KnowledgeQA`、`Embedding`、`Rerank` 或 `VLLM`
|
||||
- 模型来源(source):`local` 或 `remote`
|
||||
- 模型参数(parameters):包括 base_url、api_key、provider 等
|
||||
- 空间ID(tenant_id):建议使用小于10000的空间ID
|
||||
|
||||
**支持的服务商(provider)**:`generic`、`openai`、`aliyun`、`zhipu`、`volcengine`、`hunyuan`、`deepseek`、`minimax`、`mimo`、`siliconflow`、`jina`、`openrouter`、`litellm`、`requesty`、`gemini`、`modelscope`、`moonshot`、`qianfan`、`qiniu`、`longcat`、`gpustack`
|
||||
|
||||
### 2. 执行 SQL 插入语句
|
||||
|
||||
```sql
|
||||
-- LLM 内置模型
|
||||
INSERT INTO models (
|
||||
id, tenant_id, name, type, source, description, parameters,
|
||||
is_default, status, is_builtin
|
||||
) VALUES (
|
||||
'builtin-llm-001', 10000, 'GPT-4', 'KnowledgeQA', 'remote',
|
||||
'内置 LLM 模型',
|
||||
'{"base_url": "https://api.openai.com/v1", "api_key": "sk-xxx", "provider": "openai"}'::jsonb,
|
||||
false, 'active', true
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- Embedding 内置模型
|
||||
INSERT INTO models (...) VALUES (...) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- Rerank 内置模型
|
||||
INSERT INTO models (...) VALUES (...) ON CONFLICT (id) DO NOTHING;
|
||||
```
|
||||
|
||||
### 3. 验证
|
||||
|
||||
```sql
|
||||
SELECT id, name, type, is_builtin, status
|
||||
FROM models WHERE is_builtin = true ORDER BY type, created_at;
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **ID 命名规范**:建议使用 `builtin-{type}-{序号}` 格式
|
||||
2. **空间ID**:建议使用第一个空间ID(通常是 10000)
|
||||
3. **参数格式**:`parameters` 字段必须是有效的 JSON 格式
|
||||
4. **幂等性**:使用 `ON CONFLICT (id) DO NOTHING`
|
||||
5. **安全性**:API Key 和 Base URL 在前端会被自动隐藏
|
||||
|
||||
## 模型配置与常见问题
|
||||
|
||||
- 如果 Embedding 模型未正确配置,会导致文档无法上传。排查步骤参见 [常见问题](../运维排障/常见问题.md)
|
||||
- 模型参数中的 `provider` 决定了 API 调用格式和认证方式
|
||||
|
||||
---
|
||||
|
||||
## 反向链接
|
||||
|
||||
- [Home](../Home.md) — Wiki 首页导航
|
||||
- [内置MCP服务管理](../核心功能/内置MCP服务管理.md) — 同为内置系统配置,模式相似
|
||||
- [常见问题](../运维排障/常见问题.md) — 模型配置相关的故障排查
|
||||
- [版本路线图](../项目概述/版本路线图.md) — 路线图中模型训练方向
|
||||
@@ -0,0 +1,104 @@
|
||||
---
|
||||
title: 开启知识图谱功能
|
||||
tags: [核心功能, 知识图谱, Neo4j, 配置]
|
||||
aliases: [知识图谱配置, KnowledgeGraphSetup]
|
||||
source: 开启知识图谱功能.md
|
||||
---
|
||||
|
||||
# 开启知识图谱功能
|
||||
|
||||
本文档介绍如何在 WeKnora 中启用并验证知识图谱(Neo4j)功能,帮助你完成从环境准备到前端配置的全流程。
|
||||
|
||||
## 前置条件
|
||||
|
||||
- 已完成 WeKnora 后端与前端的基础部署
|
||||
- 具备可用的 Docker/Docker Compose 运行环境
|
||||
- 本地或远端可访问的 Neo4j 服务(推荐使用项目自带的 Docker Compose)
|
||||
|
||||
> 快速入门参见 [知识图谱](知识图谱.md)
|
||||
|
||||
## 步骤一:配置环境变量
|
||||
|
||||
在项目根目录的 `.env` 文件中新增或修改以下变量:
|
||||
|
||||
```
|
||||
NEO4J_ENABLE=true
|
||||
NEO4J_URI=bolt://neo4j:7687
|
||||
NEO4J_USERNAME=neo4j
|
||||
NEO4J_PASSWORD=your_strong_password
|
||||
# 可选:NEO4J_DATABASE=neo4j
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `NEO4J_ENABLE` 设置为 `true` 才会启用知识图谱相关逻辑
|
||||
- `NEO4J_URI` 中的 `neo4j` 为 docker-compose 服务名,如使用外部实例请替换为实际地址
|
||||
- 如果生产环境使用密钥管理,请确保密码通过安全方式注入
|
||||
|
||||
## 步骤二:启动 Neo4j 服务
|
||||
|
||||
项目附带 Neo4j 组件,可直接用以下命令启动:
|
||||
|
||||
```bash
|
||||
docker-compose --profile neo4j up -d
|
||||
```
|
||||
|
||||
常见验证命令:
|
||||
|
||||
```bash
|
||||
docker ps | grep neo4j
|
||||
```
|
||||
|
||||
> 开发环境中 Neo4j 的启动方式参见 [开发指南](../开发部署/开发指南.md)
|
||||
|
||||
## 步骤三:重启 WeKnora 服务
|
||||
|
||||
为了让新的环境变量生效,重启后端与前端:
|
||||
|
||||
```bash
|
||||
make stop && make start
|
||||
# 或者
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
确保后端日志中出现 `neo4j` 初始化成功的提示。
|
||||
|
||||
## 步骤四:在前端启用实体/关系抽取
|
||||
|
||||
1. 登录 WeKnora 前端管理页面
|
||||
2. 打开「知识库设置」或创建新的知识库
|
||||
3. 勾选「启用实体抽取」与「启用关系抽取」开关
|
||||
4. 根据界面提示补充所需的 LLM、回调或模型参数(若有)
|
||||
|
||||
保存后,系统会在文档入库阶段自动触发实体与关系抽取任务。
|
||||
|
||||
## 步骤五:验证知识图谱
|
||||
|
||||
### 方式一:Neo4j 控制台
|
||||
|
||||
1. 访问 `http://localhost:7474`(或对应主机/端口)
|
||||
2. 使用 `.env` 中的账号密码登录
|
||||
3. 执行 `MATCH (n) RETURN n LIMIT 50;` 检查是否有新节点/关系
|
||||
|
||||
### 方式二:WeKnora 界面
|
||||
|
||||
在知识库或对话页面中上传文档后,前端应展示图谱可视化入口;对话时系统会自动根据意图查询图谱并返回补充信息。
|
||||
|
||||
## 常见问题排查
|
||||
|
||||
- **无法连接 Neo4j**:确认网络可达、`NEO4J_URI` 与用户名密码正确,并检查 Neo4j 容器日志
|
||||
- **未生成节点**:确认知识库已开启实体/关系抽取,且上传的文档已完成解析;查看后端日志中是否有抽取任务异常
|
||||
- **查询无结果**:尝试在 Neo4j 控制台执行 `CALL db.schema.visualization;` 查看 schema 是否存在,必要时重新导入文档
|
||||
|
||||
> 更多排查建议参见 [常见问题](../运维排障/常见问题.md)
|
||||
|
||||
完成以上步骤后,知识图谱功能即成功启用,可结合 RAG 及 Agent 流程提升问答质量。
|
||||
|
||||
---
|
||||
|
||||
## 反向链接
|
||||
|
||||
- [Home](../Home.md) — Wiki 首页导航
|
||||
- [知识图谱](知识图谱.md) — 知识图谱快速入门(本页的精简版)
|
||||
- [开发指南](../开发部署/开发指南.md) — 开发环境中 Neo4j 的启动与配置
|
||||
- [常见问题](../运维排障/常见问题.md) — 图谱功能相关的故障排查
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: 知识图谱
|
||||
tags: [核心功能, 知识图谱, Neo4j]
|
||||
aliases: [KnowledgeGraph, 图谱]
|
||||
source: KnowledgeGraph.md
|
||||
---
|
||||
|
||||
# 知识图谱
|
||||
|
||||
## 快速开始
|
||||
|
||||
- .env 配置相关环境变量
|
||||
- 启用 Neo4j: `NEO4J_ENABLE=true`
|
||||
- Neo4j URI: `NEO4J_URI=bolt://neo4j:7687`
|
||||
- Neo4j 用户名: `NEO4J_USERNAME=neo4j`
|
||||
- Neo4j 密码: `NEO4J_PASSWORD=password`
|
||||
|
||||
- 启动 Neo4j
|
||||
```bash
|
||||
docker-compose --profile neo4j up -d
|
||||
```
|
||||
|
||||
- 在知识库设置页面启用实体和关系提取,并根据提示配置相关内容
|
||||
|
||||
> 详细的启用步骤参见 [开启知识图谱功能](开启知识图谱功能.md)
|
||||
|
||||
## 生成图谱
|
||||
|
||||
上传任意文档后,系统会自动提取实体和关系,并生成对应的知识图谱。
|
||||
|
||||

|
||||
|
||||
## 查看图谱
|
||||
|
||||
登录 `http://localhost:7474`,执行 `match (n) return (n)` 即可查看生成的知识图谱。
|
||||
|
||||
在对话时,系统会自动查询知识图谱,并获取相关知识。
|
||||
|
||||
## 相关主题
|
||||
|
||||
- [开启知识图谱功能](../核心功能/开启知识图谱功能.md) — 知识图谱功能的完整启用指南
|
||||
- [常见问题](../运维排障/常见问题.md) — 知识图谱相关的故障排查
|
||||
|
||||
---
|
||||
|
||||
## 反向链接
|
||||
|
||||
- [Home](../Home.md) — Wiki 首页导航
|
||||
- [开启知识图谱功能](开启知识图谱功能.md) — 本页的详细版本,包含完整的启用流程
|
||||
- [开发指南](../开发部署/开发指南.md) — 开发环境中 Neo4j 的配置与启动
|
||||
@@ -0,0 +1,106 @@
|
||||
---
|
||||
title: 常见问题
|
||||
tags: [运维排障, FAQ, 故障排查, 部署]
|
||||
aliases: [FAQ, 故障排查, QA]
|
||||
source: QA.md
|
||||
---
|
||||
|
||||
# 常见问题
|
||||
|
||||
## 1. 如何查看日志?
|
||||
|
||||
```bash
|
||||
docker compose logs -f app docreader postgres
|
||||
```
|
||||
|
||||
## 2. 如何启动和停止服务?
|
||||
|
||||
```bash
|
||||
./scripts/start_all.sh # 启动服务
|
||||
./scripts/start_all.sh --stop # 停止服务
|
||||
./scripts/start_all.sh --stop && make clean-db # 清空数据库
|
||||
```
|
||||
|
||||
> 开发模式下的启停参见 [开发指南](../开发部署/开发指南.md)
|
||||
|
||||
## 3. 服务启动后无法正常上传文档?
|
||||
|
||||
通常是 Embedding 模型和对话模型没有正确被设置导致。
|
||||
|
||||
### 排查步骤
|
||||
|
||||
1. 查看 `.env` 配置中的模型信息是否配置完整:
|
||||
|
||||
```bash
|
||||
INIT_LLM_MODEL_NAME=your_llm_model
|
||||
INIT_EMBEDDING_MODEL_NAME=your_embedding_model
|
||||
INIT_EMBEDDING_MODEL_DIMENSION=your_embedding_model_dimension
|
||||
INIT_EMBEDDING_MODEL_ID=your_embedding_model_id
|
||||
```
|
||||
|
||||
2. 如果使用 remote API,还需提供 `BASE_URL` 和 `API_KEY`
|
||||
3. 如需重排序功能,额外配置 Rerank 模型
|
||||
4. 查看主服务日志,是否有 `ERROR` 日志输出
|
||||
|
||||
> 模型管理参见 [内置模型管理](../核心功能/内置模型管理.md)
|
||||
|
||||
## 4. 没有图片或者显示无效的图片链接?
|
||||
|
||||
1. 确认多模态功能已正确配置(知识库设置 → 高级设置 → 多模态功能)
|
||||
2. 确认 MinIO 服务已启动
|
||||
3. 检查 MinIO Bucket 权限
|
||||
4. 配置 `MINIO_PUBLIC_ENDPOINT`(如需从其他设备访问)
|
||||
|
||||
**重要**:Bucket 名称不要包含特殊字符;如无法修改权限,可填入不存在的 bucket 名称,系统会自动创建。
|
||||
|
||||
## 5. 平台兼容性说明
|
||||
|
||||
`OCR_BACKEND=paddle` 在部分平台上可能无法正常运行。
|
||||
|
||||
- **方案一**:关闭 OCR 识别(删除 `OCR_BACKEND` 配置)
|
||||
- **方案二**:使用外部 OCR 模型(推荐)— 配置 `OCR_BACKEND=vlm`
|
||||
|
||||
## 6. 如何使用数据分析功能?
|
||||
|
||||
- **智能推理**:需在工具配置中勾选「查看数据元信息」和「数据分析」
|
||||
- **快速问答智能体**:无需手动选择工具
|
||||
|
||||
### 注意事项
|
||||
|
||||
- 仅支持 CSV 和 Excel 格式
|
||||
- 仅支持只读查询(SELECT、SHOW 等)
|
||||
|
||||
## 7. 页面里刚保存的配置几秒后又消失了?
|
||||
|
||||
通常是浏览器代理、缓存或插件干扰导致。建议:
|
||||
1. 关闭浏览器代理和抓包工具
|
||||
2. 强制刷新或使用无痕窗口
|
||||
3. 检查 Network 面板确认请求未被代理改写
|
||||
|
||||
## 8. SSRF 校验白名单
|
||||
|
||||
`SSRF_WHITELIST` 配置项用于绕过常规 SSRF 限制,支持:精确域名、通配域名、IPv4/IPv6、CIDR。
|
||||
|
||||
```bash
|
||||
# SSRF_WHITELIST=internal.service,*.corp.example,172.16.0.0/12
|
||||
```
|
||||
|
||||
> 生产环境请谨慎配置。搜索引擎的 API 端点硬编码策略参见 [添加网络搜索引擎](../集成扩展/添加网络搜索引擎.md)
|
||||
|
||||
## 相关主题
|
||||
|
||||
- [开发指南](../开发部署/开发指南.md) — 开发环境问题排查
|
||||
- [开启知识图谱功能](../核心功能/开启知识图谱功能.md) — 知识图谱功能故障排查
|
||||
- [内置模型管理](../核心功能/内置模型管理.md) — 模型配置问题
|
||||
- [IM集成开发](../集成扩展/IM集成开发.md) — IM 集成相关问题
|
||||
|
||||
---
|
||||
|
||||
## 反向链接
|
||||
|
||||
- [Home](../Home.md) — Wiki 首页导航
|
||||
- [开发指南](../开发部署/开发指南.md) — 开发环境搭建与问题排查
|
||||
- [开启知识图谱功能](../核心功能/开启知识图谱功能.md) — 知识图谱功能相关故障排查
|
||||
- [内置模型管理](../核心功能/内置模型管理.md) — 模型配置相关问题
|
||||
- [快速开发模式](../开发部署/快速开发模式.md) — 开发模式相关问题
|
||||
- [添加网络搜索引擎](../集成扩展/添加网络搜索引擎.md) — SSRF 白名单与搜索 API 安全策略
|
||||
@@ -0,0 +1,130 @@
|
||||
---
|
||||
title: IM集成开发
|
||||
tags: [集成扩展, IM, 企微, 飞书, Lark, Slack, Telegram, 钉钉, Mattermost]
|
||||
aliases: [IM集成, IM开发, 企业微信, 即时通讯]
|
||||
source: IM集成开发文档.md
|
||||
---
|
||||
|
||||
# IM 集成开发
|
||||
|
||||
WeKnora 的 IM 集成模块将企业即时通讯平台(企业微信、飞书、Lark、Slack、Telegram、钉钉、Mattermost)接入 WeKnora 知识问答管道,支持在 IM 中直接向 AI 提问并获得实时流式回答。
|
||||
|
||||
IM 渠道绑定到 Agent,一个 Agent 可接入多个 IM 渠道。
|
||||
|
||||
> IM 渠道中的 Agent 可以使用 [MCP 工具](../核心功能/MCP功能使用说明.md) 和 [Skills 技能](../核心功能/Agent技能系统.md)
|
||||
|
||||
## 支持的平台
|
||||
|
||||
| 平台 | WebSocket 模式 | Webhook 模式 | 流式输出 |
|
||||
|------|:-:|:-:|:-:|
|
||||
| 企业微信 | ✅ | ✅ | ✅ |
|
||||
| 飞书 | ✅ | ✅ | ✅ (CardKit) |
|
||||
| Lark(飞书国际版) | ✅ | ✅ | ✅ (CardKit) |
|
||||
| Slack | ✅ (Socket Mode) | ✅ (Events API) | ✅ |
|
||||
| Telegram | ✅ (长轮询) | ✅ | ✅ |
|
||||
| 钉钉 | ✅ (Stream) | ✅ | ✅ (AI 卡片) |
|
||||
| Mattermost | — | ✅ | ✅ |
|
||||
|
||||
## 快速接入指南
|
||||
|
||||
### 前置条件
|
||||
|
||||
- WeKnora 已部署并运行
|
||||
- 已创建至少一个 Agent(自定义智能体)
|
||||
- Agent 已配置好模型和知识库
|
||||
|
||||
> Agent 配置模型参见 [内置模型管理](../核心功能/内置模型管理.md)
|
||||
|
||||
### 企业微信接入
|
||||
|
||||
提供两种模式:
|
||||
- **WebSocket 模式**(智能机器人,推荐)— 无需公网域名
|
||||
- **Webhook 模式**(自建应用)— 需要公网回调地址
|
||||
|
||||
### 飞书接入
|
||||
|
||||
- **WebSocket 模式**(推荐)— 无需公网域名
|
||||
- **Webhook 模式** — 需要公网回调地址
|
||||
|
||||
> 飞书同时也是数据源导入的支持平台,参见 [数据源导入开发](数据源导入开发.md)
|
||||
|
||||
### Lark 接入
|
||||
|
||||
Lark 是飞书的国际版,IM 接口一致,与飞书共用同一套适配器。接入步骤基本相同,但开放平台
|
||||
与**权限清单**不同:
|
||||
|
||||
- 飞书:<https://open.feishu.cn/>
|
||||
- Lark:<https://open.larksuite.com/>
|
||||
|
||||
> **权限不可套用飞书的清单**:飞书清单混入了数据源连接器(Wiki 同步)用的权限,其中部分在
|
||||
> Lark 并不存在,整份导入会失败。Lark 只需 IM 相关的 6 项,见
|
||||
> [IM集成开发文档 — Lark 权限配置](../../IM集成开发文档.md#lark-权限配置)。
|
||||
|
||||
> 两朵云的应用互不通用,凭证只在创建它的那朵云上有效。
|
||||
|
||||
### Slack 接入
|
||||
|
||||
- **Socket Mode**(推荐)— 无需公网域名
|
||||
- **Events API** — 需要公网回调地址
|
||||
|
||||
### Telegram 接入
|
||||
|
||||
- **长轮询模式**(推荐)— 无需公网域名
|
||||
- **Webhook 模式** — 需要 HTTPS 公网回调
|
||||
|
||||
### 钉钉接入
|
||||
|
||||
- **Stream 模式**(推荐)— 无需公网域名
|
||||
- **Webhook 模式** — 需要公网回调地址
|
||||
|
||||
### Mattermost 接入
|
||||
|
||||
- 仅支持 **Webhook 模式**(出站 Webhook + REST API v4)
|
||||
|
||||
## 架构设计
|
||||
|
||||
系统采用 **Adapter Pattern**,每个平台实现 `im.Adapter` 接口,通过 `AdapterFactory` 动态创建。核心设计模式包括:
|
||||
|
||||
| 模式 | 用途 |
|
||||
|------|------|
|
||||
| Adapter Pattern | 统一不同 IM 平台的差异 |
|
||||
| Factory Pattern | 从数据库渠道配置动态创建 Adapter |
|
||||
| Command Pattern | 可插拔的斜杠指令系统 |
|
||||
| Producer-Consumer | QA 队列 + Worker Pool |
|
||||
|
||||
## 斜杠指令系统
|
||||
|
||||
| 指令 | 说明 |
|
||||
|------|------|
|
||||
| `/help` | 显示所有可用指令 |
|
||||
| `/info` | 查看当前绑定智能体信息 |
|
||||
| `/search` | 对知识库执行混合检索 |
|
||||
| `/stop` | 取消当前 QA 请求 |
|
||||
| `/clear` | 清空当前对话记忆 |
|
||||
|
||||
## 扩展新平台
|
||||
|
||||
接入新的 IM 平台只需 3 步:
|
||||
|
||||
1. 实现 `im.Adapter` 接口(可选 `StreamSender`、`FileDownloader`)
|
||||
2. 注册适配器工厂
|
||||
3. 前端添加平台选项
|
||||
|
||||
> 扩展开发模式与 [添加网络搜索引擎](添加网络搜索引擎.md) 和 [集成向量数据库](集成向量数据库.md) 类似
|
||||
|
||||
## 相关主题
|
||||
|
||||
- [数据源导入开发](../集成扩展/数据源导入开发.md) — 飞书数据源同步(共享飞书应用凭证)
|
||||
- [MCP功能使用说明](../核心功能/MCP功能使用说明.md) — Agent 可调用 MCP 工具
|
||||
- [Agent技能系统](../核心功能/Agent技能系统.md) — Agent 可使用 Skills 技能
|
||||
- [开发指南](../开发部署/开发指南.md) — 开发环境搭建
|
||||
|
||||
---
|
||||
|
||||
## 反向链接
|
||||
|
||||
- [Home](../Home.md) — Wiki 首页导航
|
||||
- [数据源导入开发](../集成扩展/数据源导入开发.md) — 同样涉及飞书集成,可共享应用凭证
|
||||
- [MCP功能使用说明](../核心功能/MCP功能使用说明.md) — IM 中 Agent 可调用 MCP 工具
|
||||
- [Agent技能系统](../核心功能/Agent技能系统.md) — IM 中 Agent 可使用 Skills
|
||||
- [版本路线图](../项目概述/版本路线图.md) — IM 集成已完成的里程碑
|
||||