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
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
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
# Web 前端(frontend/)
|
||||
|
||||
WeKnora 的 Web 前端是一个基于 **Vue 3 + TypeScript + Vite** 的单页应用(SPA),承载知识库管理、Agent 对话、组织协作、系统设置等全部交互界面。同一份代码同时服务三种形态:
|
||||
|
||||
1. **标准 Web 部署**:Vite 构建产物由 nginx 容器托管,`/api` 反向代理到后端;
|
||||
2. **网页嵌入(Embed)**:独立的轻量入口 `frontend/embed.html` + `frontend/src/embed-main.ts`,供第三方网站以 iframe / 浮窗方式嵌入智能体对话;
|
||||
3. **桌面端(Wails)**:通过 `frontend/src/wailsjs/` 下的自动生成绑定与桌面进程的 Go 侧通信,前端代码中可见大量对桌面形态的适配(如 `--wails-draggable` 拖拽区域、窗口深浅色同步)。
|
||||
|
||||
## 技术栈总览
|
||||
|
||||
依据 `frontend/package.json`(版本 0.8.0):
|
||||
|
||||
| 类别 | 选型 | 版本 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| 框架 | Vue | ^3.5.34 | Composition API,`<script setup>` 风格 |
|
||||
| 语言 | TypeScript | ~6.0.3 | `vue-tsc` 做类型检查(`npm run type-check`) |
|
||||
| 构建工具 | Vite | ^7.3.5 | 插件:`@vitejs/plugin-vue`、`@vitejs/plugin-vue-jsx` |
|
||||
| UI 组件库 | TDesign (tdesign-vue-next) | ^1.19.2 | 配合 `tdesign-icons-vue-next` 0.4.4(版本被 overrides 锁定) |
|
||||
| 状态管理 | Pinia | ^3.0.4 | 全部 store 位于 `frontend/src/stores/` |
|
||||
| 路由 | Vue Router | ^4.5.0 | `createWebHistory`,见 `frontend/src/router/index.ts` |
|
||||
| 多语言 | vue-i18n | ^11.4.2 | zh-CN / en-US / ru-RU / ko-KR |
|
||||
| HTTP | axios | ^1.16.0 | 统一实例封装于 `frontend/src/utils/request.ts` |
|
||||
| SSE 流式 | @microsoft/fetch-event-source | ^2.0.1 | 聊天流式回复,见 `frontend/src/api/chat/streame.ts` |
|
||||
| Markdown 渲染 | marked / marked-katex-extension / katex / highlight.js / mermaid | — | 聊天答案富文本渲染(公式、代码高亮、图表) |
|
||||
| 安全 | dompurify | ^3.4.11 | v-html 内容统一消毒(`frontend/src/utils/markdownDomPurify.ts`) |
|
||||
| 文档预览 | docx-preview / @vue-office/pptx / xlsx / papaparse | — | 站内预览 Word / PPT / Excel / CSV |
|
||||
| 长列表 | vue-virtual-scroller | 2.0.0-beta.8 | 消息列表虚拟滚动 |
|
||||
| 样式 | Less + CSS Variables | less ^4.6.4 | 主题变量见 `frontend/src/assets/theme/theme.css` |
|
||||
|
||||
值得注意的依赖细节:
|
||||
|
||||
- `xlsx` 不走 npm registry,而是安装本地 tarball:`"xlsx": "file:./packages/xlsx-0.20.2.tgz"`(即 `frontend/packages/` 目录的用途,锁定版本、离线可装);
|
||||
- `frontend/pnpm-workspace.yaml` 并非声明子包 workspace,只包含 `allowBuilds` 白名单(允许 `@vue-office/pptx`、`esbuild`、`vue-demi` 执行构建脚本),用于 pnpm 的构建脚本安全策略;
|
||||
- `overrides` / `resolutions` 中禁用了 `lightningcss` 并统一 `esbuild`、`serialize-javascript` 版本。
|
||||
|
||||
## 模块结构
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph entries["构建入口 (vite.config.ts 双入口)"]
|
||||
MAIN["index.html + src/main.ts<br/>(主 SPA)"]
|
||||
EMBED["embed.html + src/embed-main.ts<br/>(嵌入渠道 /embed/:channelId)"]
|
||||
end
|
||||
|
||||
subgraph app["应用层"]
|
||||
ROUTER["路由 (src/router/index.ts)<br/>导航守卫: 登录 / 租户 / SystemAdmin"]
|
||||
VIEWS["视图层 (src/views)<br/>knowledge / chat / agent / settings / organization / embed ..."]
|
||||
COMP["通用组件 (src/components)"]
|
||||
end
|
||||
|
||||
subgraph state["状态与逻辑层"]
|
||||
STORES["Pinia stores (src/stores)<br/>auth / settings / organization ..."]
|
||||
COMPOSABLES["composables (src/composables)<br/>useTheme / useFont / useChatStreamHandler ..."]
|
||||
HOOKS["hooks (src/hooks)"]
|
||||
UTILS["utils (src/utils)<br/>request.ts / markdown 渲染 / 安全消毒"]
|
||||
end
|
||||
|
||||
subgraph io["数据访问层"]
|
||||
API["API 封装 (src/api)<br/>axios 实例 + SSE 流式"]
|
||||
I18N["多语言 (src/i18n)<br/>zh-CN / en-US / ru-RU / ko-KR"]
|
||||
WAILS["桌面绑定 (src/wailsjs)<br/>Wails 自动生成"]
|
||||
end
|
||||
|
||||
BACKEND["WeKnora 后端 API<br/>(/api, /files)"]
|
||||
|
||||
MAIN --> ROUTER --> VIEWS
|
||||
EMBED --> VIEWS
|
||||
VIEWS --> COMP
|
||||
VIEWS --> STORES
|
||||
VIEWS --> COMPOSABLES
|
||||
COMPOSABLES --> UTILS
|
||||
STORES --> API
|
||||
VIEWS --> API
|
||||
API --> BACKEND
|
||||
VIEWS --> I18N
|
||||
COMPOSABLES --> WAILS
|
||||
```
|
||||
|
||||
### 目录速览
|
||||
|
||||
| 目录 | 职责 |
|
||||
| --- | --- |
|
||||
| `frontend/src/main.ts` | 主 SPA 入口:安装 TDesign / Pinia / Router / i18n,初始化主题与字体,注册 TDesign 图标离线保护(`installTDesignIconOfflineGuard`,避免运行时请求 `tdesign.gtimg.com`),等待 `router.isReady()` 后再挂载以避免首屏闪烁 |
|
||||
| `frontend/src/embed-main.ts` | 嵌入入口:独立的 Vue 应用与独立路由(仅 `/embed/:channelId`),挂载 `#embed-app`,使用独立 i18n(`src/i18n/embed.ts`) |
|
||||
| `frontend/src/views/` | 页面级组件,按业务域分目录(见下方路由表) |
|
||||
| `frontend/src/components/` | 跨页面通用组件(消息气泡、上传遮罩、命令面板等) |
|
||||
| `frontend/src/stores/` | Pinia 状态(见下方 store 表) |
|
||||
| `frontend/src/api/` | 后端 API 封装(见下方 API 模块表) |
|
||||
| `frontend/src/composables/` | 组合式函数:主题、字体、聊天流处理、引用弹层、Embed 桥接等 |
|
||||
| `frontend/src/hooks/` | 业务 hook(如 `useKnowledgeBase`) |
|
||||
| `frontend/src/utils/` | 工具集:axios 实例、markdown 渲染管线、DOMPurify 消毒、Agent 工具展示等 |
|
||||
| `frontend/src/i18n/` | vue-i18n 配置与语言包 |
|
||||
| `frontend/src/assets/theme/` | 主题 CSS 变量(light / dark) |
|
||||
| `frontend/src/wailsjs/` | Wails 桌面端自动生成绑定(勿手改) |
|
||||
| `frontend/src/directives/`、`frontend/src/types/`、`frontend/src/config/` | 自定义指令、类型定义、配置 |
|
||||
| `frontend/public/` | 静态资源:`weknora-widget.js`(第三方站点嵌入加载器)、`config.js`(运行时配置占位,容器启动时覆盖)、离线 TDesign 图标 |
|
||||
| `frontend/packages/` | 本地依赖 tarball(`xlsx-0.20.2.tgz`) |
|
||||
|
||||
## 页面路由清单
|
||||
|
||||
路由定义在 `frontend/src/router/index.ts`,使用 `createWebHistory`,所有页面组件均为动态 import(按路由分包懒加载)。
|
||||
|
||||
### 顶层路由
|
||||
|
||||
| 路径 | 名称 | 组件 | 功能 |
|
||||
| --- | --- | --- | --- |
|
||||
| `/` | — | 重定向 | 重定向到 `/platform/knowledge-bases` |
|
||||
| `/login` | `login` | `src/views/auth/Login.vue` | 登录页(含 OIDC、语言切换、动画背景) |
|
||||
| `/register` | `registerByInvite` | `src/views/auth/Login.vue` | 邀请注册落地页——复用 Login 组件,挂载时检测 `?token=xxx` 切换到邀请注册模式 |
|
||||
| `/onboarding/workspace` | `workspaceOnboarding` | `src/views/auth/WorkspaceOnboarding.vue` | 无租户用户的工作空间引导页(创建或等待被邀请),需要登录但不要求已有租户 |
|
||||
| `/join` | `joinOrganization` | 重定向 | 加入组织邀请链接,把 `?code=` 转成 `invite_code` 参数并跳到 `/platform/organizations` |
|
||||
| `/knowledgeBase` | `home` | `src/views/knowledge/KnowledgeBase.vue` | 知识库详情(历史遗留顶层路径) |
|
||||
| `/platform` | `Platform` | `src/views/platform/index.vue` | 平台主布局(左侧菜单 + 路由出口 + 全局设置模态 + 拖拽上传遮罩),默认重定向到知识库列表 |
|
||||
| `/platform/dev/markdown` | `markdownTest` | `src/views/dev/MarkdownTestPage.vue` | 仅开发模式(`import.meta.env.DEV`)注册的 Markdown 渲染视觉回归测试页 |
|
||||
|
||||
### `/platform` 子路由
|
||||
|
||||
| 路径 | 名称 | 组件 | 功能 |
|
||||
| --- | --- | --- | --- |
|
||||
| `/platform/knowledge-bases` | `knowledgeBaseList` | `src/views/knowledge/KnowledgeBaseList.vue` | 知识库列表:空间侧栏(全部/我的/按组织/收藏/最近)、卡片列表、创建入口 |
|
||||
| `/platform/knowledge-bases/:kbId` | `knowledgeBaseDetail` | `src/views/knowledge/KnowledgeBase.vue` | 知识库详情:文档列表、上传、解析状态、会话入口、Wiki 等 |
|
||||
| `/platform/agents` | `agentList` | `src/views/agent/AgentList.vue` | 智能体(Agent)列表与管理,编辑走 `AgentEditorModal.vue` |
|
||||
| `/platform/creatChat` | `globalCreatChat` | `src/views/creatChat/creatChat.vue` | 新建对话页:推荐问题、选择知识库/Agent/模型后发起会话 |
|
||||
| `/platform/knowledge-bases/:kbId/creatChat` | `kbCreatChat` | `src/views/creatChat/creatChat.vue` | 从某个知识库上下文发起新对话(同一组件) |
|
||||
| `/platform/chat/:chatid` | `chat` | `src/views/chat/index.vue` | 会话页:消息流(SSE 流式渲染、骨架屏、虚拟滚动)、引用面板、附件预览 |
|
||||
| `/platform/organizations` | `organizationList` | `src/views/organization/OrganizationList.vue` | 组织列表:创建/加入组织、成员与共享资源管理(配合 `OrganizationSettingsModal.vue`) |
|
||||
| `/platform/settings` | `settings` | `src/views/settings/Settings.vue` | 设置中心(全屏模态形态),分区见下方「设置中心的分区与可见性」 |
|
||||
| `/platform/tenant` | — | 重定向 | 兼容旧路径 → `/platform/settings` |
|
||||
| `/platform/knowledge-search` | — | 重定向 | 旧全局搜索路径 → 知识库列表并通过 `?cmdk=` 打开全局命令面板(⌘K) |
|
||||
| `/platform/integrations` | — | 重定向 | → `/platform/settings?section=integration-im`(旧 `?tab=` 会归一成 `integration-<tab>`;视图在 `src/views/integrations/`) |
|
||||
| `/platform/system`、`/platform/system/settings`、`/platform/system/admins` | `systemSettings` / `systemAdmins` | 重定向 | 系统管理旧路径 → `/platform/settings?section=system-global`,要求 `requiresSystemAdmin`(视图在 `src/views/system/`:`SystemSettings.vue`、`SystemAuditLog.vue`、`PlatformAPIKeys.vue` 等) |
|
||||
| `/platform/system/queues` | `systemQueues` | 重定向 | → `/platform/settings?section=runtime-queues`(运行时任务队列 `src/views/system/RuntimeQueues.vue`) |
|
||||
|
||||
### 独立入口:嵌入页
|
||||
|
||||
`/embed/:channelId` 不属于主 SPA 路由,而是由 `frontend/embed.html` + `frontend/src/embed-main.ts` 构成的独立入口(nginx 与 Vite dev server 都将 `/embed/*` fallback 到 `embed.html`),组件为 `src/views/embed/EmbedPage.vue`(配套 `EmbedChatView.vue` / `EmbedChatCore.vue` / `EmbedBotMessage.vue` 等),使用 Embed token 鉴权,供第三方网站 iframe 嵌入。
|
||||
|
||||
### 设置中心的分区与可见性
|
||||
|
||||
`Settings.vue` 把所有分区按七组呈现,用 `?section=` 定位:
|
||||
|
||||
| 分组 | 分区(`section` 值) |
|
||||
| --- | --- |
|
||||
| 账户 | `general`(个人偏好)、`userprofile` |
|
||||
| 空间 | `tenant`(空间信息)、`members`(成员)、`chathistory` |
|
||||
| 模型与运行 | `models`、`ollama`、`weknoracloud` |
|
||||
| 发布与集成 | `integration-im`、`integration-embed`、`integration-api`、`integration-chrome`、`integration-claw` |
|
||||
| 数据与扩展 | `vectorstore`、`parser`、`storage`、`websearch`、`mcp` |
|
||||
| 系统管理 | `system-global`、`runtime-queues`、`platform-api-keys`、`system-audit-log` |
|
||||
| 平台 | `system`(版本信息) |
|
||||
|
||||
可见性由两套规则决定,且**前端只做收敛展示,后端路由守卫才是权威**:
|
||||
|
||||
- **空间角色门槛**:`frontend/src/config/settingsAccess.ts` 的 `SETTINGS_SECTION_MIN_ROLE` 给每个分区规定最低角色。`general` / `models` / `system` / `userprofile` / `tenant` / `members` 是 `viewer` 起(只读可见),其余(`ollama`、`weknoracloud`、`websearch`、`chathistory`、`vectorstore`、`parser`、`storage`、`mcp`)要求 `admin`。另有 `SETTINGS_MANAGEMENT_SHORTCUT_MIN_ROLE`:头像菜单里那些标着「管理」的快捷入口门槛更高(成员管理要 `owner`,模型管理要 `admin`),避免把只读页面伪装成管理入口。
|
||||
- **系统管理员白名单**:`SYSTEM_ADMIN_SETTINGS_SECTIONS`(`system-global`、`runtime-queues`、`platform-api-keys`、`system-audit-log`)只对系统管理员显示,与空间角色无关,详见[租户、用户与认证授权](../03-features/01-tenant-auth.md)的「系统管理员与平台控制台」。
|
||||
|
||||
### 知识库编辑弹窗的分区
|
||||
|
||||
不少配置**不在设置中心,而在知识库编辑弹窗里**(`KnowledgeBaseEditorModal.vue`),因为它们是按库生效的。侧栏分区按五组组织,其中三个只在「编辑已有知识库」时出现:
|
||||
|
||||
| 分组 | 分区(`key`) | 备注 |
|
||||
| --- | --- | --- |
|
||||
| 基础 | `basic`、`models` | 名称、类型、对话/向量/摘要模型 |
|
||||
| 处理 | `parser`、`multimodal`、`asr`、`chunking` | 解析引擎与首行表头、图片理解、语音转写、分块参数 |
|
||||
| 数据 | `vectorStore`、`storage`、`faq` | `faq` 仅 FAQ 类型库;`vectorStore` 绑定后不可改 |
|
||||
| 集成 | `datasource` | **仅编辑模式**,飞书 / Notion / 语雀 / RSS 同步配在这里,不在全局设置里 |
|
||||
| 管理 | `graph`、`advanced`、`share`、`activity` | 知识图谱、高级项、共享到组织、活动流;后两个仅编辑模式 |
|
||||
|
||||
### 全局命令面板(⌘K / Ctrl+K)
|
||||
|
||||
`components/GlobalCommandPalette.vue` 是除侧栏之外的第二条主要导航通路:
|
||||
|
||||
- 搜索知识库、文档与会话,支持把范围收窄到某个知识库(scope chip)后再搜;
|
||||
- 空状态下展示最近搜索与快捷动作(建库、上传、新建会话等);
|
||||
- 右上角的入口打开**检索设置抽屉**(`views/settings/RetrievalSettings.vue`)。这是调 TopK、向量/关键词阈值、重排参数的地方——它**不在设置中心里**,找不到的话就是在这。
|
||||
|
||||
### 导航守卫
|
||||
|
||||
`router.beforeEach` 中实现了一条完整的鉴权链(`frontend/src/router/index.ts`):
|
||||
|
||||
1. **OIDC 回调放行**:URL hash 含 `oidc_result=` / `oidc_error=` 时直接放行,交由 `App.vue` 消费;
|
||||
2. **Lite / 桌面端深链恢复**:Lite 模式硬刷新落在默认首页时,从 `sessionStorage` 恢复上次访问的 `/platform` 子路径;
|
||||
3. **会话恢复**:未登录时先用 `localStorage` 中的 `weknora_token` 调 `getCurrentUser()` 恢复会话(同时刷新 memberships,避免角色变更滞后);
|
||||
4. **Lite 自动登录**:恢复失败则尝试一次 `autoSetup()`(单机版免登录),失败会在 `localStorage` 打标避免重复尝试;
|
||||
5. **租户门槛**:已登录但无有效租户 → 跳 `/onboarding/workspace`;
|
||||
6. **SystemAdmin 门槛**:`requiresSystemAdmin` 路由对非系统管理员跳回知识库列表(仅 UI 层拦截,服务端另有强校验)。
|
||||
|
||||
## 状态管理(Pinia)
|
||||
|
||||
`frontend/src/stores/` 下的 store 与辅助模块:
|
||||
|
||||
| 文件 | Store ID / 类型 | 职责 |
|
||||
| --- | --- | --- |
|
||||
| `stores/auth.ts` | `useAuthStore` | 认证核心:user / token / refreshToken / tenant / memberships / 角色判断(`hasRole`、`isSystemAdmin`)、Lite 模式标记;登出时级联清理其他 store 的空间级缓存并按用户重载偏好(主题/字体) |
|
||||
| `stores/chatResources.ts` | `useChatResourcesStore` | 空间级资源缓存(TTL 60s):知识库、Agent、模型、Web 搜索 provider 列表,供聊天/新建对话选择器复用 |
|
||||
| `stores/editorResources.ts` | `useEditorResourcesStore` | 编辑器/设置相关资源缓存(TTL 60s):存储引擎配置与状态、Prompt 模板、解析引擎、系统信息、MCP 服务、Skill、Agent 类型预设、检索配置 |
|
||||
| `stores/commandPalette.ts` | `useCommandPaletteStore` | 全局命令面板(⌘K / Ctrl+K)开关与查询;最近搜索按 (user, tenant) 作用域存储避免跨账号泄漏 |
|
||||
| `stores/organization.ts` | `useOrganizationStore` | 组织协作:组织列表、成员、共享知识库/Agent、加入申请与审核、角色升级等全套动作 |
|
||||
| `stores/organizationState.ts` | 纯函数模块 | 组织列表 upsert / merge、加入审核对成员数影响等纯逻辑(配套单测 `organizationState.test.ts`) |
|
||||
| `stores/settings.ts` | 设置 store | 会话与 Agent 配置:选中的知识库/文件/标签/MCP/Skill/工具、模型配置、Ollama 配置、Web 搜索开关等 |
|
||||
| `stores/settingsStorage.ts` | 纯函数模块 | 设置持久化(`WeKnora_settings` key)的读取、克隆与内建 Agent 模式修复(配套 `settingsStorage.test.mjs`) |
|
||||
| `stores/menu.ts` | `useMenuStore` | 左侧导航菜单结构(新建对话、知识库、Agent 等条目)与 i18n 标题 |
|
||||
| `stores/knowledge.ts` | `knowledgeStore` | 知识卡片列表与总数(轻量) |
|
||||
| `stores/ui.ts` | `useUIStore` | 全局 UI 状态:设置模态、知识库编辑模态、手工文档编辑器、侧栏折叠等开关与参数 |
|
||||
| `stores/uploadConfirm.ts` | 上传确认 store | 上传/URL 导入/手工录入/重新解析前的处理参数确认对话框状态 |
|
||||
| `stores/versionedRequest.ts` | 纯函数模块 | `createVersionedRequestCoordinator`:带版本号的缓存请求协调器,防止旧响应覆盖新写入(配套 `versionedRequest.test.ts`) |
|
||||
|
||||
## API 封装(frontend/src/api/)
|
||||
|
||||
### 请求基座
|
||||
|
||||
- **axios 实例**:`frontend/src/utils/request.ts` 创建统一实例(`baseURL` 来自 `frontend/src/utils/api-base.ts` 的 `getApiBaseUrl()`,尊重 Vite `BASE_URL` 以支持子路径反代部署;超时 30s)。
|
||||
- **请求拦截器**:自动附加 `Authorization: Bearer <weknora_token>`(Embed 渠道的 `Embed ` token 不被覆盖)、`Accept-Language`(当前 i18n 语言)、`X-Request-ID`(随机串)、`X-Tenant-ID`(跨空间访问,始终携带激活空间 id 以避免切空间后 header 丢失)。
|
||||
- **响应拦截器**:2xx 解包返回 `data`;401 触发单飞(single-flight)refresh token 刷新,失败队列重放;公开端点(`/auth/login`、`/auth/auto-setup`、`/auth/invitations/lookup`、`/api/v1/embed/` 等 `PUBLIC_AUTH_PATHS`)的 401 直接抛给页面而不跳登录;Embed 页面永不重定向到 `/login`。
|
||||
- **SSE 流式**:`frontend/src/api/chat/streame.ts` 基于 `@microsoft/fetch-event-source` 封装 `useStream()`,支持流式输出、加载态、错误态与请求调试元数据;上层由 `frontend/src/composables/useChatStreamHandler.ts` 组织为聊天消息流。
|
||||
|
||||
### 模块清单
|
||||
|
||||
| 模块 | 职责 |
|
||||
| --- | --- |
|
||||
| `api/auth/` | 登录、注册、OIDC、`autoSetup`(Lite 免登录)、`getCurrentUser` 会话恢复 |
|
||||
| `api/tenant/`(`index` / `members` / `invitations` / `audit-log`) | 租户(工作空间)信息、成员管理、邀请、审计日志 |
|
||||
| `api/organization/` | 组织 CRUD、成员、共享知识库/Agent、加入申请 |
|
||||
| `api/knowledge-base/` | 知识库 CRUD 与文件/知识条目管理 |
|
||||
| `api/chat/`(`index` / `streame` / `temporary-attachments`) | 会话 CRUD、标题生成、SSE 流式问答、临时附件 |
|
||||
| `api/chat-history.ts` | 聊天历史记录 |
|
||||
| `api/agent/` | 自定义 Agent CRUD、类型预设、占位符(含内建 Quick Answer / Smart Reasoning id) |
|
||||
| `api/model/` | 模型配置管理 |
|
||||
| `api/retrieval.ts` | 租户检索配置 |
|
||||
| `api/vector-store.ts` / `api/storage-backend.ts` / `api/chunker/` | 向量库、存储后端、分块器配置 |
|
||||
| `api/datasource/` | 数据源接入 |
|
||||
| `api/embed/` | 网页嵌入渠道管理(创建渠道、限流等) |
|
||||
| `api/initialization/` | 系统初始化流程 |
|
||||
| `api/system/` | 系统信息、存储引擎状态、Prompt 模板、解析引擎等系统级接口 |
|
||||
| `api/mcp-service.ts` / `api/skill/` | MCP 服务与 Skill 管理 |
|
||||
| `api/web-search.ts` / `api/web-search-provider.ts` | Web 搜索及 provider 配置 |
|
||||
| `api/wiki/` | 知识库 Wiki 生成相关接口 |
|
||||
| `api/message-suggestion.ts` | 推荐问题 |
|
||||
| `api/user-favorites.ts` | 用户收藏(知识库/Agent 收藏列表) |
|
||||
|
||||
## 对话时间线的等待态
|
||||
|
||||
RAG 流水线的可视化进度(`views/chat/components/RagPipelineProgress.vue`)在「所有可见步骤都完成」到「模型吐出第一个字」之间会有一段静默期。这段空白由 `utils/rag-pipeline-state.ts` 描述:
|
||||
|
||||
- `getRagPipelineWaitKind()` 判定等待类型:检索步骤确实完成过才叫 `model`(正在生成回答);纯附件问答这类没有检索步骤的轮次给中性的 `preparing`,而不是完全没有反馈;
|
||||
- `createRagWaitController()` 负责呈现细节:延迟 `RAG_WAIT_REVEAL_DELAY_MS`(250ms)才显示,避免模型很快回答时闪一下;超过 `RAG_WAIT_STALL_DELAY_MS`(60s)转为「停滞」态——SSE 断连时后端不会再发 `is_completed`,没有这个上限进度条会永远宣称「马上就好」;
|
||||
- 状态变化通过一个常驻的 `aria-live` 区域播报,读屏用户不会因为节点整体替换而漏读。
|
||||
|
||||
## 多语言(i18n)
|
||||
|
||||
实现于 `frontend/src/i18n/index.ts`,基于 `vue-i18n`(`legacy: false` 的 Composition 模式,`globalInjection: true`):
|
||||
|
||||
- **支持语言**(`frontend/src/i18n/locales/`):
|
||||
- `zh-CN`(简体中文,默认与 fallback)
|
||||
- `en-US`(英语)
|
||||
- `ru-RU`(俄语)
|
||||
- `ko-KR`(韩语)
|
||||
- 语言选择持久化在 `localStorage` 的 `locale` key;axios 拦截器会把当前语言写入 `Accept-Language` 请求头,使后端返回本地化内容。
|
||||
- 因部分翻译刻意内嵌 `<strong>` 标记(经 DOMPurify 消毒后 v-html 渲染),配置了 `warnHtmlMessage: false` 关闭 vue-i18n 的 HTML 告警。
|
||||
- **Embed 独立 i18n**:访客侧嵌入页使用单独的 `frontend/src/i18n/embed.ts`(由 `embed-main.ts` 加载),管理端「网页嵌入」文案仍在主语言包中;`frontend/src/i18n/locales/embed/index.ts` 统一 re-export 语言归一化助手(支持从 URL 参数同步 embed 语言)。
|
||||
- **审计与裁剪工具**:语言包体量大、容易积累无人引用的死键或漏翻的新键,因此配套了三个脚本(`frontend/package.json`):
|
||||
|
||||
| 命令 | 作用 |
|
||||
| --- | --- |
|
||||
| `npm run check-i18n` | 跑 `src/i18n/localeKeyAudit.test.ts`,校验各语言包键集一致、无缺失引用 |
|
||||
| `npm run scan-i18n-gaps` | 扫描源码中实际用到的 key 与语言包对比,报告未定义与未使用的键 |
|
||||
| `npm run regenerate-i18n-locales` | 按扫描结果重新生成裁剪后的语言包 |
|
||||
|
||||
审计日志的动作名走单独的注册表(`i18n/auditActionRegistry.ts` + `auditActionLocaleDefaults.ts`),新增审计动作时在注册表补一条即可,避免裁剪工具把它们当成未引用的死键删掉。
|
||||
|
||||
## 主题与外观
|
||||
|
||||
- **主题模式**:`frontend/src/composables/useTheme.ts` 提供 `light | dark | system` 三态。生效方式是在 `document.documentElement` 上设置 `theme-mode` 属性;`system` 模式监听 `prefers-color-scheme` 媒体查询自动跟随。
|
||||
- **CSS 变量**:`frontend/src/assets/theme/theme.css` 以 TDesign token 体系(`--td-brand-color-*`、`--td-bg-color-*`、`--td-text-color-*`、字体/圆角/阴影等)分别定义 `:root[theme-mode="light"]` 与 `:root[theme-mode="dark"]` 两套变量,品牌色为绿色系;组件样式一律引用变量实现一键换肤。
|
||||
- **偏好持久化**:主题与字体偏好通过 `frontend/src/composables/preferenceStorage.ts` 按用户 id 命名空间存入 `localStorage`,登录/登出/切换账号时由 `reloadThemeFromStorage()` / `reloadFontFromStorage()` 重载(在 `stores/auth.ts` 中触发)。
|
||||
- **字体**:`frontend/src/composables/useFont.ts` 管理界面字体选择,`main.ts` 启动时 `initTheme()` + `initFont()`。
|
||||
- **桌面端同步**:`useTheme.ts` 中的 `syncWailsNativeChrome()` 调用 Wails runtime 的 `WindowSetDarkTheme / WindowSetLightTheme / WindowSetBackgroundColour`,让原生窗口底色与网页主题一致,减轻刷新白闪。
|
||||
|
||||
## 构建与部署
|
||||
|
||||
### 开发与构建(vite.config.ts)
|
||||
|
||||
`frontend/vite.config.ts` 要点:
|
||||
|
||||
- **双入口构建**:`rollupOptions.input` 同时构建 `index.html`(主 SPA)与 `embed.html`(嵌入页);开发环境用自定义插件 `embedHtmlDevFallback()` 把 `/embed/:channelId` 请求改写到 `/embed.html`,与 nginx 行为对齐。
|
||||
- **代码分包**:`manualChunks` 将 mermaid/dagre/cytoscape、marked/katex、highlight.js 分别拆为 `vendor-mermaid`、`vendor-markdown`、`vendor-highlight`;embed 入口通过 `modulePreload.resolveDependencies` 过滤重型聊天 chunk,保证嵌入页首屏只加载 token 交换所需代码。
|
||||
- **版本注入**:`__FRONTEND_VERSION__`(package.json version)与 `__FRONTEND_COMMIT__`(`VITE_FRONTEND_COMMIT` / `GITHUB_SHA` / `git rev-parse`)编译期注入。
|
||||
- **开发代理**:dev server(端口 5173)与 preview(端口 4173)都把 `/api`、`/files` 代理到 `VITE_DEV_PROXY_TARGET`(或 `FRONTEND_BACKEND_URL`,默认 `http://localhost:8080`)。
|
||||
- **别名**:`@` → `frontend/src`;并对 `@vue-office/pptx` 做入口文件探测修正。
|
||||
- 常用脚本:`npm run dev` / `npm run build` / `npm run preview`(用生产构建产物本地起服务,最接近发布镜像的验证环境)/ `npm run type-check` / `npm run test`(tsx --test)。
|
||||
|
||||
### 生产镜像(Dockerfile + nginx)
|
||||
|
||||
`frontend/Dockerfile`:
|
||||
|
||||
- 基础镜像固定为 digest 锁定的 `nginx:1.30.3-alpine`(注释明确禁止改回浮动 tag——更新的 Alpine 3.24+ 在 CentOS 7 旧内核上无法启动,曾导致 v0.7.0 故障);
|
||||
- 静态产物需先在宿主机构建(`./scripts/build_frontend_dist.sh`),镜像只 `COPY dist`;
|
||||
- `nginx.conf` 作为模板放入 `/etc/nginx/templates/default.conf.template`,暴露 80 端口,入口为 `docker-entrypoint.sh`。
|
||||
|
||||
`frontend/docker-entrypoint.sh`(运行时配置注入):
|
||||
|
||||
1. 生成 `/usr/share/nginx/html/config.js`,把 `MAX_FILE_SIZE_MB`(默认 50)与 `DEFAULT_LOCALE`(可选,默认空)写入 `window.__RUNTIME_CONFIG__` 供前端运行时读取;entrypoint 仅允许 `zh-CN|en-US|ru-RU|ko-KR`,非法值会被丢弃;
|
||||
2. 用 `envsubst` 渲染 nginx 模板,可配置环境变量:`MAX_FILE_SIZE_MB`、`DEFAULT_LOCALE`、`APP_HOST`(默认 `app`)、`APP_PORT`(默认 `8080`)、`APP_SCHEME`(默认 `http`,远程 HTTPS 后端可设 `https`);
|
||||
3. 前台启动 nginx。
|
||||
|
||||
`frontend/nginx.conf` 关键行为:
|
||||
|
||||
- **SPA fallback**:`/` 下 `try_files ... /index.html`,且 `index.html` 设置 `no-cache`(避免升级后用户拿到旧版本);带 hash 的 `/assets/*` 设置一年 immutable 缓存;
|
||||
- **API 代理**:`/api/` 与 `/files` 反代到 `${APP_SCHEME}://${APP_HOST}:${APP_PORT}`,`/api/` 针对 SSE 关闭 `proxy_buffering` / 缓存 / 分块编码,读写超时放宽到 3600s,并配置 3 次 upstream 重试;
|
||||
- **资源短链 `/r/`**:`location ^~ /r/` 同样反代到后端。IM 渠道把 `resource://` 图片改写成 `<APP_EXTERNAL_URL>/r/<token>`,缺这段配置时请求会落进 SPA fallback,IM 侧图片显示为空白(详见 [IM 集成](../03-features/12-im-integration.md));
|
||||
- **嵌入页**:`/embed/*` fallback 到 `embed.html`(独立 location,不继承主站的 `X-Frame-Options: SAMEORIGIN`,因此可被第三方 iframe 加载);`/weknora-widget.js` 是给第三方站点的静态加载器;文件头部另附可选的独立 embed 子域 server 块示例;
|
||||
- 启用 gzip(注释记录了实测收益:低带宽下首屏从 25s 降到 3-5s)及一组安全响应头(`X-Frame-Options`、`X-Content-Type-Options`、`Referrer-Policy` 等,在各 location 内重复声明以规避 nginx `add_header` 不继承的问题)。
|
||||
|
||||
## 桌面端(Wails)关联
|
||||
|
||||
`frontend/src/wailsjs/` 是 Wails 框架自动生成的绑定代码(文件头标注 "automatically generated. DO NOT EDIT"):
|
||||
|
||||
- `wailsjs/go/main/App.d.ts` / `App.js`:Go 侧 `App` 结构体方法的 JS 绑定,包括 `CheckForUpdates` / `AutoCheckForUpdates`(桌面更新检查)、`GetAPIBaseURL` / `GetAPILanBaseURL`、桌面内置 HTTP 服务的端口与对外监听设置(`GetDesktopHTTPPortSetting`、`SetDesktopHTTPBindPublicSetting` 等);
|
||||
- `wailsjs/runtime/`:Wails runtime API(窗口控制等),前端在浏览器环境下调用会被 try/catch 安静降级(如 `useTheme.ts`)。
|
||||
|
||||
桌面应用的窗口内容就是这份前端代码,Lite 模式(`autoSetup` 免登录 + 深链恢复)与 `--wails-draggable` 标记的可拖拽标题区都是为桌面形态准备的适配。
|
||||
@@ -0,0 +1,530 @@
|
||||
# WeKnora CLI(weknora 命令行工具)
|
||||
|
||||
WeKnora CLI(二进制名 `weknora`)是 WeKnora RAG 服务的官方命令行客户端,源码位于仓库的 `cli/` 目录(独立 Go module:`github.com/Tencent/WeKnora/cli`,要求 Go 1.26+)。它面向两类使用者:
|
||||
|
||||
- **人类用户**:管理知识库(Knowledge Base)与文档、执行混合检索(hybrid search)、进行有引用溯源(grounded)的流式问答;
|
||||
- **AI Agent / 脚本**:默认输出 JSON envelope、提供类型化错误码与退出码矩阵、`--dry-run` 预演、`weknora schema` 机器可读契约,以及 `weknora mcp serve` MCP 服务器模式。
|
||||
|
||||
命令树入口在 `cli/cmd/root.go`,各命令组按目录组织在 `cli/cmd/` 下。
|
||||
|
||||
## 总体架构
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph entry["入口 (cli/main.go → cli/cmd/root.go)"]
|
||||
R["weknora 根命令<br/>全局 flag: --format / --jq / --profile / --log-level / -y"]
|
||||
end
|
||||
|
||||
subgraph groups["命令组 (cli/cmd/*)"]
|
||||
G1["profile / auth / config<br/>(连接与凭证)"]
|
||||
G2["kb / doc / chunk / link<br/>(知识库与文档)"]
|
||||
G3["search / chat / session / message<br/>(检索与对话)"]
|
||||
G4["agent / model<br/>(自定义 Agent 与模型)"]
|
||||
G5["mcp / skills / api<br/>(Agent 集成与逃生舱)"]
|
||||
G6["doctor / version / schema / exit-codes<br/>(诊断与自省)"]
|
||||
end
|
||||
|
||||
subgraph internal["内部层 (cli/internal/*)"]
|
||||
F["cmdutil.Factory<br/>(惰性构建 Config / Client / Secrets / Prompter)"]
|
||||
C["config<br/>config.yaml 多 profile"]
|
||||
S["secrets<br/>OS keyring / 0600 文件回退"]
|
||||
P["projectlink<br/>.weknora/project.yaml"]
|
||||
O["output + format<br/>JSON envelope / NDJSON / jq"]
|
||||
end
|
||||
|
||||
SRV["WeKnora Server<br/>(REST API + SSE)"]
|
||||
|
||||
R --> groups
|
||||
groups --> F
|
||||
F --> C
|
||||
F --> S
|
||||
F --> P
|
||||
groups --> O
|
||||
F -->|"SDK client (github.com/Tencent/WeKnora/client)"| SRV
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 安装
|
||||
|
||||
### 从源码构建(当前受支持的安装方式)
|
||||
|
||||
`cli/README.md` 明确说明:**从源码构建是目前受支持的安装方式**;预编译二进制、`go install`、CLI 的 Homebrew formula 计划随正式 tag 发布一同提供。
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Tencent/WeKnora.git
|
||||
cd WeKnora/cli
|
||||
go build -o weknora .
|
||||
sudo mv weknora /usr/local/bin/ # 或放到任意 $PATH 目录
|
||||
```
|
||||
|
||||
### 使用 cli/Makefile
|
||||
|
||||
`cli/Makefile` 提供带版本元数据(通过 `-ldflags` 注入 `internal/build.Version/Commit/Date`)的构建目标:
|
||||
|
||||
| target | 作用 |
|
||||
|---|---|
|
||||
| `make build` | 编译到 `./bin/weknora`,注入 `git describe` 版本、commit 短哈希与构建时间 |
|
||||
| `make test` | `go test ./...` |
|
||||
| `make test-coverage` | 测试并输出覆盖率报告 |
|
||||
| `make lint` | `go vet ./...` |
|
||||
| `make tidy` | `go mod tidy` |
|
||||
| `make clean` | 删除 `./bin` 与 coverage.out |
|
||||
|
||||
注意:Makefile 中**没有** `install` target,构建产物需自行移动到 `$PATH`。
|
||||
|
||||
### Homebrew(服务端 Lite 版,非 CLI)
|
||||
|
||||
仓库 `Formula/` 目录下目前只有一个 formula:`Formula/weknora-lite.rb`,它安装的是 **WeKnora 服务端的单二进制 Lite 版**(`weknora-lite`),而不是本文档的 `weknora` CLI。该 formula:
|
||||
|
||||
- 按 macOS/Linux × arm64/amd64 四个平台从 GitHub Releases 下载 `WeKnora-lite_v<version>_<os>_<arch>.tar.gz`;
|
||||
- 生成 `weknora-lite` 启动脚本:首次运行自动生成 `~/.config/weknora/.env.lite` 配置、数据存到 `~/.local/share/weknora/`;
|
||||
- 支持 `brew services start weknora-lite` 作为后台服务运行,日志在 `$(brew --prefix)/var/log/weknora-lite.log`。
|
||||
|
||||
在本地用 Lite 版做 CLI 的目标服务器是一个方便的组合:`brew services start weknora-lite` 起服务端,再用 `weknora profile add local --host http://localhost:8080 --use` 连接。
|
||||
|
||||
---
|
||||
|
||||
## 配置与 Profile 管理
|
||||
|
||||
### 配置文件与路径
|
||||
|
||||
用户级配置由 `cli/internal/config/config.go` 管理,路径为 `$XDG_CONFIG_HOME/weknora/config.yaml`(`XDG_CONFIG_HOME` 未设置时为 `~/.config/weknora/config.yaml`;路径解析见 `cli/internal/xdg/xdg.go`,在所有操作系统上都遵循 XDG 变量,包括 macOS)。写入使用原子写(临时文件 + rename),权限 0600。
|
||||
|
||||
on-disk schema(`config.Config` / `config.Profile`):
|
||||
|
||||
```yaml
|
||||
current_profile: prod # 当前激活的 profile 名
|
||||
profiles:
|
||||
prod:
|
||||
host: https://kb.example.com # 必填:服务器地址
|
||||
tenant_id: 42 # 可选:租户 id(仅展示用,不注入请求头)
|
||||
user: user@example.com # 可选:账号邮箱(仅 profile list 展示)
|
||||
api_key_ref: keychain://... # API key 的存储引用(keychain:// 或 file://)
|
||||
token_ref: keychain://... # JWT access token 引用
|
||||
refresh_token_ref: keychain://...
|
||||
default_kb_id: "..." # 可选:默认知识库
|
||||
defaults:
|
||||
format: json # 可选:CLI 级默认输出格式
|
||||
no_version_check: true # 可选:关闭版本兼容检查
|
||||
```
|
||||
|
||||
### 凭证存储(secrets)
|
||||
|
||||
凭证**不写入 config.yaml**,只存引用(ref)。`cli/internal/secrets/` 提供两种后端:
|
||||
|
||||
- **KeyringStore**:OS 钥匙串(macOS Keychain / Linux keyring),命名空间 `weknora:<profile>:<key>`,key 为 `access` / `refresh` / `api_key`;
|
||||
- **FileStore**:钥匙串不可用时(headless CI、无 DBus 的 WSL、容器)回退到 `$XDG_CONFIG_HOME/weknora/secrets/<profile>/<key>` 的 0600 明文文件,`auth login` 会在 stderr 打印一次性警告。
|
||||
|
||||
### 多 Profile 切换与解析优先级
|
||||
|
||||
Profile 的解析链在 `cli/internal/cmdutil/factory.go`(`Factory.ActiveProfile`)中实现,优先级从高到低:
|
||||
|
||||
1. 全局 `--profile <name>` flag(仅本次调用生效,不写盘);
|
||||
2. 环境变量 `WEKNORA_PROFILE`;
|
||||
3. `config.yaml` 中的 `current_profile`(由 `weknora profile use` 持久化切换)。
|
||||
|
||||
### 无状态环境变量凭证(headless / CI / Agent 路径)
|
||||
|
||||
`factory.go` 的 `buildClientFromEnv` 支持完全绕过 config.yaml 和钥匙串:
|
||||
|
||||
| 环境变量 | 作用 |
|
||||
|---|---|
|
||||
| `WEKNORA_TOKEN` | Bearer JWT(优先于 `WEKNORA_API_KEY`) |
|
||||
| `WEKNORA_API_KEY` | API key |
|
||||
| `WEKNORA_HOST` | 服务器地址(未设置时回退到激活 profile 的 host) |
|
||||
| `WEKNORA_PROFILE` | 覆盖激活 profile |
|
||||
| `WEKNORA_KB_ID` | 显式指定知识库 id |
|
||||
| `WEKNORA_FORMAT` | 默认输出格式(text / json / ndjson) |
|
||||
| `WEKNORA_LOG_LEVEL` | SDK 日志级别(error / warn / info / debug) |
|
||||
| `WEKNORA_AGENT_HELP=1` | `--help` 时输出机器可读的 AgentHelp JSON(`cli/internal/cmdutil/agenthelp.go`) |
|
||||
|
||||
### 知识库(--kb)解析链
|
||||
|
||||
需要知识库作用域的命令(chat、doc、chunk、search chunks/docs 等)通过 `Factory.ResolveKB` 按 4 级回退解析(`cli/internal/cmdutil/factory.go`):
|
||||
|
||||
1. `--kb` flag(UUID 直接透传;名称则经 `ListKnowledgeBases` 做名称 → id 查找,见 `cli/internal/cmdutil/kb.go`);
|
||||
2. `WEKNORA_KB_ID` 环境变量;
|
||||
3. 项目链接文件 `.weknora/project.yaml`(由 `weknora link` 写入,从当前目录向上逐级查找,最多 64 层,见 `cli/internal/projectlink/projectlink.go`);
|
||||
4. 均未命中则报 `local.kb_id_required` 错误。
|
||||
|
||||
JWT profile(同时持有 access + refresh token)会自动获得 401 透明刷新传输层(`AuthRetryTransport`):首个 401 触发 `/api/v1/auth/refresh` 并重放原请求;API key profile 与环境变量凭证不做刷新。
|
||||
|
||||
---
|
||||
|
||||
## 全局 Flag、输出格式与脚本化
|
||||
|
||||
### 全局 Flag(`cli/cmd/root.go` 的 `addGlobalFlags`)
|
||||
|
||||
| Flag | 简写 | 说明 |
|
||||
|---|---|---|
|
||||
| `--format` | | 输出格式:`text` \| `json` \| `ndjson`。**默认 `json`**(与是否 TTY 无关,agent-first 设计;人类可显式 `--format text`)。环境变量 `WEKNORA_FORMAT` 可设默认,优先级:`--format` > `WEKNORA_FORMAT` > 默认 json |
|
||||
| `--jq` | `-q` | 用 jq 表达式过滤 JSON 输出(要求 `--format json|ndjson`;与显式 `--format text` 组合报错) |
|
||||
| `--profile` | | 本次调用覆盖激活 profile(不写盘) |
|
||||
| `--log-level` | | SDK 调试日志级别:error \| warn \| info \| debug |
|
||||
| `--yes` | `-y` | 跳过破坏性操作的确认提示 |
|
||||
| `--version` | | 打印版本(等价于 `weknora version`) |
|
||||
|
||||
许多写命令还注册了 `--dry-run`(`cli/internal/cmdutil/dryrun.go`),覆盖 kb/doc/agent/model/profile/session/link/api/skills 等几乎全部 mutation 命令:不执行任何写操作,输出 `meta.dry_run=true` + `meta.plan`(将要执行的动作描述)。
|
||||
|
||||
### JSON Envelope 输出契约(`cli/internal/output/envelope.go`)
|
||||
|
||||
成功路径写 stdout:
|
||||
|
||||
```json
|
||||
{"ok": true, "data": ..., "meta": {"count": 2, "total_count": 2, "has_more": false}, "profile": "prod"}
|
||||
```
|
||||
|
||||
- `data`:命令负载(对象或数组);`--jq` 投影须以 `.data` 为根,如 `--jq '.data[].id'`;
|
||||
- `meta`:列表命令携带 `count` / `total_count` / `has_more`;批量操作携带 `successes` / `failures` 及三态 `status`(success / partial / error);dry-run 携带 `dry_run` + `plan`;
|
||||
- `profile`:本次解析出的 profile 名。
|
||||
|
||||
错误路径写 stderr(stdout 保持干净,便于 `| jq` 管道):
|
||||
|
||||
```json
|
||||
{"ok": false, "error": {"type": "auth.unauthenticated", "message": "...", "exit_code": 3,
|
||||
"hint": "...", "retry_argv": ["weknora","auth","login"], "retryable": false}}
|
||||
```
|
||||
|
||||
错误类型是分层字符串(`cli/internal/cmdutil/errors.go`):`auth.*`、`resource.*`、`input.*`、`server.*`、`network.error`、`operation.*`、`local.*`、`internal.error`。`retry_argv` 是可直接 exec 的修复命令数组。
|
||||
|
||||
`--format ndjson` 用于流式命令(`chat` / `session ask` / `session resume`):首行注入 CLI `init` 事件(含 session_id、kb_id、profile),之后逐行透传 SDK SSE 事件(`cli/internal/sse/`)。
|
||||
|
||||
### 退出码矩阵(`cli/cmd/exitcodes.go`,可运行 `weknora exit-codes` 获取机器可读版本)
|
||||
|
||||
| 退出码 | 含义 | 对应错误类型 | Agent 建议动作 |
|
||||
|---|---|---|---|
|
||||
| 0 | 成功 | — | 继续 |
|
||||
| 1 | 类型化本地错误 / 操作失败 / 未分类 | `local.*`, `operation.failed`, `operation.cancelled`, `server.session_create_failed`, `internal.error` | 读 stderr 后决定重试/放弃 |
|
||||
| 2 | flag / 参数解析错误(未知 flag、参数个数、缺必填 flag) | `input.invalid_argument`(与退出码 5 同类型,靠退出码区分) | 查 `weknora <cmd> --help` |
|
||||
| 3 | 认证 / 授权失败 | `auth.*` | 重新 `weknora auth login` 后重试 |
|
||||
| 4 | 资源不存在 | `resource.not_found` | 核对资源 id |
|
||||
| 5 | 输入值非法(类型化校验,非解析错误) | `input.*`(除 confirmation_required) | 调整参数重试 |
|
||||
| 6 | 限流 | `server.rate_limited` | 退避后重试 |
|
||||
| 7 | 服务器 / 网络错误 | `server.*`, `network.*` | 瞬态错误,退避重试 |
|
||||
| 10 | 需要确认(高风险写操作) | `input.confirmation_required` | 询问人类;获明确批准后加 `-y` 重试 |
|
||||
| 124 | 操作超时 | `operation.timeout` | 提高 `--timeout` 或检查底层任务 |
|
||||
| 130 | 被信号取消(SIGINT/SIGTERM) | — | 停止,不要重试 |
|
||||
|
||||
**高风险写保护(exit-10 协议)**:删除类、`kb config set`、`api -X DELETE/PUT/PATCH`、`message delete`、`session tool-approval resolve` 等命令在非 TTY / JSON 场景下若未加 `-y`,直接以退出码 10 返回 `input.confirmation_required` 且不执行任何变更 —— agent 无法静默修改服务器状态。
|
||||
|
||||
### 机器自省
|
||||
|
||||
- `weknora schema`(`cli/cmd/schema.go`):无参数列出所有叶子命令 + 用途索引;`weknora schema kb create` 输出单个命令的完整契约(used_for、flags、examples、output、risk);
|
||||
- `WEKNORA_AGENT_HELP=1 weknora <cmd> --help`:输出同源的 AgentHelp JSON;
|
||||
- 未知子命令输出类型化 `input.unknown_subcommand` envelope,含 `suggestions`(did-you-mean)与可用子命令列表。
|
||||
|
||||
---
|
||||
|
||||
## 命令组详解
|
||||
|
||||
以下每组对应 `cli/cmd/` 下的一个目录。
|
||||
|
||||
### profile — 管理连接目标(`cli/cmd/profile/`)
|
||||
|
||||
| 子命令 | Use | 说明 |
|
||||
|---|---|---|
|
||||
| list | `list` | 列出已配置的 profile |
|
||||
| add | `add <name>` | 注册新 profile(只记 host,不含凭证) |
|
||||
| use | `use <name>` | 持久化切换默认 profile |
|
||||
| remove | `remove <name>` | 删除 profile(清除 config 条目与钥匙串引用) |
|
||||
|
||||
`add` 的关键 flag:`--host`(必填,服务器 URL)、`--user`(可选展示用邮箱)、`--use`(添加后立即切换)。
|
||||
|
||||
```bash
|
||||
weknora profile add prod --host=https://kb.example.com --use
|
||||
weknora profile list --format json
|
||||
```
|
||||
|
||||
### auth — 凭证管理(`cli/cmd/auth/`)
|
||||
|
||||
| 子命令 | Use | 说明 |
|
||||
|---|---|---|
|
||||
| login | `login` | 认证**当前激活 profile**:交互式邮箱+密码,或 `--with-token` 从 stdin 读 API key(会先调 `/auth/me` 校验再持久化) |
|
||||
| logout | `logout` | 清除某 profile 的存储凭证;`--all` 清除全部 profile |
|
||||
| list | `list` | 列出认证 profile |
|
||||
| status | `status` | 显示激活 profile、principal 与 token 状态 |
|
||||
| refresh | `refresh` | 用存储的 refresh token 换新 JWT access token |
|
||||
| token | `token` | 把激活 profile 的原始凭证打印到 stdout(shell 脚本用) |
|
||||
|
||||
```bash
|
||||
weknora auth login # 交互式(TTY)
|
||||
echo "$WEKNORA_API_KEY" | weknora auth login --with-token # 非交互 / agent
|
||||
weknora auth status --format json
|
||||
```
|
||||
|
||||
注意:`auth login` 不接受 `--host`,必须先 `profile add ... --use` 创建激活 profile。
|
||||
|
||||
### config — 查看解析后的配置(`cli/cmd/config/`)
|
||||
|
||||
| 子命令 | Use | 说明 |
|
||||
|---|---|---|
|
||||
| view | `view` | 只读展示解析后的配置及**每个值的来源**(active_profile / profile_source / auth_source / host / kb_id / kb_source / log_level / format_default / config_file / secrets / project_link 等),全程不发网络请求 |
|
||||
|
||||
```bash
|
||||
weknora config view --format json --jq '.data.kb_source'
|
||||
```
|
||||
|
||||
### link / unlink — 目录绑定知识库(`cli/cmd/link/`)
|
||||
|
||||
`link` 与 `unlink` 都直接挂在根命令下(见 `root.go`)。
|
||||
|
||||
| 命令 | Use | 说明 |
|
||||
|---|---|---|
|
||||
| link | `link [kb]` | 在当前目录写 `.weknora/project.yaml`,绑定 KB(位置参数或 `--kb`,等价;TTY 下不传参进入交互选择;已有链接直接覆盖)。支持 `--dry-run` |
|
||||
| unlink | `unlink` | 删除当前目录的 KB 绑定 |
|
||||
|
||||
```bash
|
||||
weknora link engineering # 名称自动解析为 id
|
||||
weknora link --kb a32a63ff-fb36-4874-bcaa-30f48570a694
|
||||
```
|
||||
|
||||
### kb — 知识库管理(`cli/cmd/kb/`)
|
||||
|
||||
| 子命令 | Use | 说明 |
|
||||
|---|---|---|
|
||||
| list | `list` | 列出可见知识库;`--pinned` 只看置顶,`--limit/-L`(默认 30) |
|
||||
| view | `view <kb-id>` | 按 ID 查看 |
|
||||
| create | `create <name>` | 创建;`--description`、`--embedding-model`、`--chat-model`(创建即可用)、`--storage-provider` |
|
||||
| update | `update <kb-id>` | 改名/描述:`--name`、`--description`(在 `kb/edit.go`) |
|
||||
| delete | `delete <kb-id>` | 删除(exit-10 确认保护,`-y` 跳过) |
|
||||
| pin / unpin | `pin <kb-id>` / `unpin <kb-id>` | 置顶/取消置顶(幂等:已处于目标状态则 no-op) |
|
||||
| status | `status <kb-id>` | 浅健康检查(1 次 HTTP) |
|
||||
| check | `check <kb-id>` | 端到端校验(状态 + 失败文档聚合) |
|
||||
| config | `config <kb-id>` | 只读查看模型配置(embedding/llm/rerank/multimodal,`retrieval_ready` 标志,绝不显示 API key) |
|
||||
| config set | `set <kb-id>` | 绑定模型:`--chat-model` 与 `--embedding-model` 均必填(id 或名称);高风险写,exit-10 保护 |
|
||||
|
||||
```bash
|
||||
weknora kb create docs --embedding-model text-embedding-3 --chat-model gpt-4o
|
||||
weknora kb config set <kb-id> --chat-model <id> --embedding-model <id> -y
|
||||
```
|
||||
|
||||
### doc — 文档管理(`cli/cmd/doc/`)
|
||||
|
||||
| 子命令 | Use | 说明 |
|
||||
|---|---|---|
|
||||
| upload | `upload <file>` | 上传本地文件;`--name`、`--recursive` + `--glob`(目录批量,如 `'*.pdf'`)、`--metadata key=value`(可重复)、`--enable-multimodel`、`--channel` |
|
||||
| fetch | `fetch <url>` | 抓取远程文档;`--name`、`--title`、`--file-type`(URL 无扩展名时的类型提示)、`--tag-id`、`--channel` |
|
||||
| create | `create` | 用内联 Markdown 文本建条目:`--text`(必填)、`--title`、`--tag-id`、`--channel` |
|
||||
| list | `list` | 列表;`--status pending|processing|completed|failed`、`--keyword`、`--file-type`、`--source`、`--tag-id`、`--start-time/--end-time`(RFC3339)、`--limit/-L`、`--page-size`、`--all-pages` |
|
||||
| view | `view <doc-id>` | 查看文档 |
|
||||
| update | `update <doc-id>` | `--title`、`--description` |
|
||||
| delete | `delete <doc-id> [<doc-id>...] \| --all --kb=<kb-id>` | 批量删除 / 清空 KB(exit-10 保护) |
|
||||
| download | `download <doc-id>` | 下载原文件;`-O/--output`(`-` 到 stdout)、`--clobber` |
|
||||
| reparse | `reparse <doc-id>` | 重新解析 |
|
||||
| wait | `wait <doc-id> [<doc-id>...]` | 轮询等待解析完成;`--timeout`(默认 10m,超时退出码 124)、`--interval`(默认 2s,指数退避封顶 15s) |
|
||||
|
||||
```bash
|
||||
weknora doc upload ./design.pdf --kb docs
|
||||
weknora doc wait <doc-id> --timeout 5m && weknora search chunks "RRF" --kb docs
|
||||
```
|
||||
|
||||
### chunk — 分块调试(`cli/cmd/chunk/`)
|
||||
|
||||
| 子命令 | Use | 说明 |
|
||||
|---|---|---|
|
||||
| list | `list` | 枚举文档分块(管理/调试用途,非检索):`--doc`(必填)、`--limit/-L`、`--page-size`、`--all-pages` |
|
||||
| view | `view <chunk-id>` | 查看单个分块内容 |
|
||||
| delete | `delete <chunk-id> [<chunk-id>...] --doc <doc-id>` | 删除分块(`--doc` 必填;exit-10 保护) |
|
||||
|
||||
### search — 检索(`cli/cmd/search/`)
|
||||
|
||||
| 子命令 | Use | 说明 |
|
||||
|---|---|---|
|
||||
| chunks | `chunks "<query>"` | **混合检索**(向量 + 关键词):`--kb`、`--limit/-L`(默认 8,为 RAG 上下文窗口调优)、`--vector-threshold`、`--keyword-threshold`、`--no-vector`、`--no-keyword` |
|
||||
| docs | `docs "<query>"` | 按关键词找文档(服务端过滤):`--kb`、`--limit`、`--page-size`、`--all-pages` |
|
||||
| kb | `kb "<query>"` | 按名称/描述找知识库(客户端子串匹配):`--limit` |
|
||||
| sessions | `sessions "<query>"` | 按标题/描述找会话(客户端子串匹配):`--limit`、`--page-size`、`--all-pages` |
|
||||
|
||||
```bash
|
||||
weknora search chunks "rate limiting design" --kb docs --limit 5 --format json --jq '.data[].content'
|
||||
```
|
||||
|
||||
### chat — 流式 RAG 问答(`cli/cmd/chat/chat.go`)
|
||||
|
||||
单命令:`chat "<text>"`。三种输出模式共享一次 SDK 流式调用:
|
||||
|
||||
- `--format json`(默认):把流投影缓冲为单个 envelope(events、session_id、assistant_message_id 等);
|
||||
- `--format text`:实时人类可读回答流;
|
||||
- `--format ndjson`:原始 SSE 事件透传(首行 init 事件带 session_id / kb_id)。
|
||||
|
||||
flag:`--kb`、`--session`(续接已有会话)、`--reference`(带引用索引)、`--verbose`(带 reasoning / 工具 / 生命周期事件)。
|
||||
|
||||
```bash
|
||||
weknora chat "What is RRF?" --kb a32a63ff-fb36-4874-bcaa-30f48570a694
|
||||
weknora chat "继续" --session sess_abc --format ndjson
|
||||
```
|
||||
|
||||
### session — 会话管理(`cli/cmd/session/`)
|
||||
|
||||
| 子命令 | Use | 说明 |
|
||||
|---|---|---|
|
||||
| list | `list` | 会话列表:`--limit/-L`、`--page-size`、`--all-pages`、`--since`(如 7d / 24h / 30m) |
|
||||
| view | `view <session-id>` | 查看会话;`--full` 连同聊天记录一起加载、`--limit/-L` |
|
||||
| ask | `ask "<text>"` | 向**服务端自定义 Agent** 提问:`-a/--agent`(必填)、`--session`、`--reference`、`--verbose` |
|
||||
| resume | `resume <session-id>` | 续接进行中/已完成消息的 SSE 事件流:`-m/--message`(必填) |
|
||||
| stop | `stop <session-id>` | 停止某条 assistant 消息的生成:`-m/--message`(必填) |
|
||||
| delete | `delete <session-id> [<session-id>...]` | 批量删除(exit-10 保护) |
|
||||
| tool-approval resolve | `resolve <pending-id>` | 批准/拒绝 Agent 运行中挂起的工具调用:`--reject`、`--reason`、`--modified-args`(JSON,仅批准时);高风险写 |
|
||||
|
||||
```bash
|
||||
weknora session ask "总结这个 KB" --agent agt_123 --format ndjson
|
||||
weknora session tool-approval resolve <pending-id> --reject --reason "不允许写操作" -y
|
||||
```
|
||||
|
||||
### message — 会话内消息(`cli/cmd/message/`)
|
||||
|
||||
| 子命令 | Use | 说明 |
|
||||
|---|---|---|
|
||||
| list | `list --session <session-id>` | 列消息(新→旧,时间游标分页):`--session`(必填)、`--limit/-L`、`--before`(RFC3339) |
|
||||
| search | `search "<query>"` | 跨会话搜索聊天历史(问答对):`--limit/-L`(默认 20)、`--mode keyword|vector|hybrid`、`--session`(可重复,限定范围) |
|
||||
| delete | `delete <message-id> --session <session-id>` | 删除单条消息(`--session` 必填;高风险写,exit-10 保护) |
|
||||
|
||||
### agent — 自定义 Agent CRUD(`cli/cmd/agent/`)
|
||||
|
||||
| 子命令 | Use | 说明 |
|
||||
|---|---|---|
|
||||
| list | `list` | 列表:`--limit/-L` |
|
||||
| view | `view <agent-id>` | 查看配置 |
|
||||
| create | `create <name>` | 创建:`--model`(必填,除非 `--generate-skeleton`)、`--description`、`--system-prompt` / `--system-prompt-file`(互斥,`-` 读 stdin)、`--agent-mode`、`--attach-kb`(可重复)、`--kb-selection-mode`、`--rerank-model`、`--temperature`、`--from`(复制已有 Agent)、`--config-file`(完整 AgentConfig YAML/JSON)、`--generate-skeleton`(输出空白配置骨架) |
|
||||
| update | `update <agent-id>` | 更新(`agent/edit.go`):`--name`、`--description`、`--model`、`--system-prompt(-file)`、`--agent-mode`、`--rerank-model`、`--temperature`、`--add-kb` / `--remove-kb`(可重复、幂等)、`--kb-selection-mode`、`--config-file`(整体替换基线后再叠加细粒度 flag) |
|
||||
| delete | `delete <agent-id>` | 删除(exit-10 保护) |
|
||||
| status | `status <agent-id>` | 健康状态 |
|
||||
| check | `check <agent-id>` | 端到端校验(状态 + kb_scope 可达性) |
|
||||
|
||||
```bash
|
||||
weknora agent create researcher --model gpt-4o --attach-kb <kb-id> --system-prompt-file ./prompt.md
|
||||
weknora agent update agt_123 --add-kb <kb-id2> --temperature 0.3
|
||||
```
|
||||
|
||||
### model — 模型管理(`cli/cmd/model/`)
|
||||
|
||||
| 子命令 | Use | 说明 |
|
||||
|---|---|---|
|
||||
| list | `list` | 列表:`--type`(Embedding / Rerank / KnowledgeQA / VLLM / ASR)、`--source`(local / remote / openai / aliyun …)、`--limit/-L` |
|
||||
| view | `view <model-id>` | 查看 |
|
||||
| create | `create <name>` | 注册模型:`--type`(必填;`chat` 等价 KnowledgeQA)、`--source`(必填;local=Ollama,remote=provider API)、`--provider`(source=remote 时必填)、`--base-url`、`--api-key-stdin`(从 stdin 读 key,不进 argv/history)、`--dimension`(Embedding 专用)、`--default`、`--param key=value`(可重复,值按 JSON 解析)、`--display-name`、`--description` |
|
||||
| update | `update <model-id>` | `--display-name`、`--description`、`--base-url`、`--api-key-stdin`(轮换 key)、`--param`、`--default` |
|
||||
| delete | `delete <model-id>` | 删除(exit-10 保护) |
|
||||
|
||||
```bash
|
||||
weknora model create bge-m3 --type Embedding --source local --base-url http://localhost:11434 --dimension 1024
|
||||
echo "$OPENAI_KEY" | weknora model create gpt-4o --type chat --source remote --provider openai --api-key-stdin
|
||||
```
|
||||
|
||||
### api — 原始 HTTP 逃生舱(`cli/cmd/api/api.go`)
|
||||
|
||||
单命令:`api <path>`。自动携带激活 profile 的认证 / 租户 / request-id 头。
|
||||
|
||||
| Flag | 简写 | 说明 |
|
||||
|---|---|---|
|
||||
| `--method` | `-X` | HTTP 方法(默认 GET;提供 body 时自动升级为 POST) |
|
||||
| `--data` | `-d` | 内联 JSON body(与 `--input` / `-F` 互斥) |
|
||||
| `--input` | | 从文件读 body(`-` 为 stdin) |
|
||||
| `--field` | `-F` | `key=value` 组装 JSON 对象 body(可重复;true/false/null/数字自动类型化) |
|
||||
| `--paginate` | | 跟随 offset 分页(?page=N&page_size=M),合并为单个 `{data, total}` 响应 |
|
||||
|
||||
`-X DELETE` 受 exit-10 destructive 确认保护;`PUT/PATCH` 受写确认保护;`POST` 与 typed create 一致不设门槛。支持 `--dry-run`(仅限非 GET)。
|
||||
|
||||
```bash
|
||||
weknora api /api/v1/knowledge-bases # GET
|
||||
weknora api /api/v1/knowledge-bases -d '{"name":"foo"}' # POST(自动)
|
||||
weknora api /api/v1/knowledge-bases/<id> -X DELETE -y
|
||||
```
|
||||
|
||||
### mcp — Model Context Protocol 服务器(`cli/cmd/mcp/`)
|
||||
|
||||
| 子命令 | Use | 说明 |
|
||||
|---|---|---|
|
||||
| serve | `serve` | 在 stdin/stdout 上运行 JSON-RPC 2.0 MCP 服务器(当前仅 stdio 传输);日志走 stderr;启动即急切构建 SDK client,无 profile 时以 `auth.unauthenticated` 立即失败 |
|
||||
|
||||
暴露**精选 10 个工具**(实现见 `cli/internal/mcp/tools.go`):`kb_list` / `kb_view` / `doc_list` / `doc_view` / `doc_download` / `search_chunks` / `chunk_list` / `agent_list` 为只读;`chat` 与 `session_ask` 会创建会话/消息记录。破坏性动词(create / delete / upload)被刻意排除。
|
||||
|
||||
MCP 客户端注册示例(写入客户端的 `mcpServers` 配置):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"weknora": { "command": "weknora", "args": ["mcp", "serve"] }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### skills — 内嵌 Agent Skills(`cli/cmd/skills/skills.go`)
|
||||
|
||||
| 子命令 | Use | 说明 |
|
||||
|---|---|---|
|
||||
| list | `list` | 列出二进制内嵌(`cli/skills/embed.go`)的 Agent Skills(name / description / files) |
|
||||
| install | `install` | 把内嵌 skills 写入 Agent 的 skills 目录:`--dir`(默认 `~/.claude/skills`,支持 `~` 展开)、`--force`(覆盖已存在文件,否则跳过);支持 `--dry-run` |
|
||||
|
||||
```bash
|
||||
weknora skills install --dry-run --format json
|
||||
weknora skills install --dir ~/.claude/skills --force
|
||||
```
|
||||
|
||||
### doctor — 自检(`cli/cmd/doctor/doctor.go`)
|
||||
|
||||
单命令:`doctor`。运行 4 项检查:base URL 可达性、认证、服务器版本兼容、凭证存储。每项状态为 `ok / warn / fail / skip`;任一 `fail` → 退出码 1(JSON 数据仍会输出);仅 warn → 退出码 0 但 `summary.all_passed=false`。
|
||||
|
||||
flag:`--no-cache`(绕过 `$XDG_CACHE_HOME/weknora/server-info.yaml` 缓存强制重探测)、`--offline`(跳过网络检查,仅验本地钥匙串/文件存储)。
|
||||
|
||||
```bash
|
||||
weknora doctor --format json --jq '.data.summary.all_passed'
|
||||
```
|
||||
|
||||
### 根级辅助命令(`cli/cmd/root.go`、`schema.go`、`exitcodes.go`)
|
||||
|
||||
| 命令 | Use | 说明 |
|
||||
|---|---|---|
|
||||
| version | `version` | 构建元数据(version / commit / date) |
|
||||
| schema | `schema [command...]` | 机器可读命令契约(见上文"机器自省") |
|
||||
| exit-codes | `exit-codes` | 退出码矩阵(JSON 或表格) |
|
||||
|
||||
---
|
||||
|
||||
## 验收测试覆盖了什么(`cli/acceptance/`)
|
||||
|
||||
`cli/acceptance/` 是 CLI 的跨切面契约/集成测试层(`doc.go` 注明"contract surface — change with care"),分两个子包:
|
||||
|
||||
### contract/ — 线协议契约测试
|
||||
|
||||
- **`wire_test.go`**:在进程内驱动完整 cobra 命令树,对每个场景捕获 stdout/stderr,并与 `testdata/wire/` 下的 JSON golden 文件逐字节比对。覆盖的场景包括:`version`、`auth_status`(成功 + `auth.unauthenticated` 失败)、`doctor`(offline 成功 + 网络错误)、`kb_list`(成功 / 空列表 / `auth.forbidden`)、`kb_view`(成功 / `resource.not_found`)、`profile_use`、`search`(成功 / `input.invalid` / not_found)。golden 文件固化了完整 envelope 形状(如 `{"ok":true,"data":[...],"meta":{"count":2,"total_count":2}}`),任何 wire 契约漂移都会立刻被发现;失败用例断言 stderr 包含预期的类型化错误码。
|
||||
- **`errorcodes_test.go`**:用 go/ast 扫描 `cli/cmd/` 中每一处 `cmdutil.NewError(CodeXxx, ...)` / `Wrapf(CodeXxx, ...)` 字面引用,验证错误码全部登记在 `cmdutil.AllCodes()` 注册表中 —— 保证文档化的错误码清单与代码不脱节。
|
||||
|
||||
### e2e/ — 真实服务器端到端测试
|
||||
|
||||
`e2e_test.go` 带 `//go:build acceptance_e2e` 构建标签,默认 `go test ./...` 不运行;显式执行方式:
|
||||
|
||||
```bash
|
||||
cd cli
|
||||
WEKNORA_E2E_HOST=https://kb.example.com WEKNORA_E2E_TOKEN=eyJ... \
|
||||
go test -tags=acceptance_e2e -v ./acceptance/e2e/...
|
||||
```
|
||||
|
||||
`TestRAGFullLoop` 编译真实 CLI 二进制,通过 `WEKNORA_HOST`/`WEKNORA_TOKEN` 环境变量凭证路径(验证了无钥匙串的 headless 认证链路)驱动完整 RAG 闭环:**kb create(带模型绑定)→ doc upload → doc wait(等待索引)→ search → chat**,每一步解析上一步的 JSON envelope 提取 id,同时校验功能行为与 wire 契约稳定性;临时 KB 通过 `t.Cleanup` 保证测试失败也会清理。
|
||||
|
||||
此外 `cli/cmd/` 下还有横切的树级测试(非 acceptance 目录,但同样约束整树行为):`required_positional_coverage_test.go`、`dryrun_coverage_test.go`、`agenthelp_coverage_test.go`、`root_unknown_subcommand_test.go` 等,确保每个叶子命令的位置参数校验、`--dry-run` 支持、AgentHelp 元数据与未知子命令处理全覆盖。
|
||||
|
||||
---
|
||||
|
||||
## 5 分钟上手
|
||||
|
||||
```bash
|
||||
# 1. 注册服务器为 profile 并激活
|
||||
weknora profile add prod --host https://kb.example.com --use
|
||||
|
||||
# 2. 认证(交互式;agent 场景用 --with-token)
|
||||
weknora auth login
|
||||
|
||||
# 3. 自检
|
||||
weknora doctor
|
||||
|
||||
# 4. 建库、绑定模型、传文档、等索引
|
||||
weknora kb create docs --embedding-model <emb> --chat-model <llm>
|
||||
weknora doc upload ./design.pdf --kb docs
|
||||
weknora doc wait <doc-id>
|
||||
|
||||
# 5. 检索与问答
|
||||
weknora search chunks "rate limiting" --kb docs
|
||||
weknora chat "总结这篇设计文档" --kb docs
|
||||
```
|
||||
@@ -0,0 +1,608 @@
|
||||
# Go SDK
|
||||
|
||||
WeKnora 官方 Go SDK 位于仓库的 `client/` 目录,是一个独立的 Go module,封装了 WeKnora 服务端 `/api/v1/*` 全部主要资源的 CRUD 操作与 SSE 流式对话能力。服务端自身、官方 CLI(`weknora`)均基于此 SDK 构建。
|
||||
|
||||
## 安装
|
||||
|
||||
SDK 的 module 路径定义在 `client/go.mod`:
|
||||
|
||||
```
|
||||
module github.com/Tencent/WeKnora/client
|
||||
|
||||
go 1.24.2
|
||||
```
|
||||
|
||||
安装方式:
|
||||
|
||||
```bash
|
||||
go get github.com/Tencent/WeKnora/client
|
||||
```
|
||||
|
||||
导入:
|
||||
|
||||
```go
|
||||
import "github.com/Tencent/WeKnora/client"
|
||||
```
|
||||
|
||||
## 初始化与认证
|
||||
|
||||
核心类型与构造函数定义在 `client/client.go`。
|
||||
|
||||
### Client 结构
|
||||
|
||||
```go
|
||||
type Client struct {
|
||||
baseURL string
|
||||
httpClient *http.Client
|
||||
streamTimeout time.Duration
|
||||
apiKey string
|
||||
bearerToken string
|
||||
tenantID *uint64
|
||||
}
|
||||
```
|
||||
|
||||
通过 `NewClient(baseURL string, options ...ClientOption) *Client` 创建实例。默认的普通请求超时为 30 秒;流式(SSE)请求默认**无超时**,生命周期由 `context` 控制(除非显式调用 `WithTimeout`)。
|
||||
|
||||
### ClientOption 一览
|
||||
|
||||
| Option | 说明 |
|
||||
|---|---|
|
||||
| `WithAPIKey(key string)` | 设置长期有效的 API Key,以 `X-API-Key` 请求头发送 |
|
||||
| `WithBearerToken(token string)` | 设置短期 JWT,以 `Authorization: Bearer <token>` 请求头发送(通常在 `Login` 成功后使用) |
|
||||
| `WithToken(token string)` | **Deprecated**:`WithAPIKey` 的 v0.x 兼容别名,将在下个大版本移除 |
|
||||
| `WithTimeout(timeout time.Duration)` | 同时设置普通请求与流式请求的超时上限 |
|
||||
| `WithTransport(rt http.RoundTripper)` | 替换底层 `http.RoundTripper`(用于重试/埋点/签名等中间件);传 `nil` 恢复 `http.DefaultTransport` |
|
||||
| `WithTenantID(tenantID uint64)` | 在每个请求上附加 `X-Tenant-ID` 请求头,仅用于具备 `CanAccessAllTenants` 权限的跨租户显式访问 |
|
||||
|
||||
### 认证方式说明
|
||||
|
||||
SDK 支持两种凭证,可同时配置,HTTP 层 `X-API-Key` 优先:
|
||||
|
||||
- **API Key**(长期):`WithAPIKey`,请求头 `X-API-Key`;
|
||||
- **Bearer JWT**(短期):`WithBearerToken`,请求头 `Authorization: Bearer <token>`,配合 `client/auth.go` 中的 `Login` / `RefreshToken` / `GetCurrentUser` 使用。
|
||||
|
||||
典型 JWT 登录流程(对应 `POST /api/v1/auth/login`):
|
||||
|
||||
```go
|
||||
c := client.NewClient("http://localhost:8080")
|
||||
loginResp, err := c.Login(ctx, client.LoginRequest{ /* email + password */ })
|
||||
// 然后用返回的 access token 重建带认证的客户端
|
||||
authed := client.NewClient("http://localhost:8080",
|
||||
client.WithBearerToken(loginResp.AccessToken))
|
||||
```
|
||||
|
||||
### 租户(Tenant)与请求头注入
|
||||
|
||||
`applyAuthHeaders`(`client/client.go`)会在每个请求上自动注入:
|
||||
|
||||
- `X-API-Key` / `Authorization`(按配置);
|
||||
- `X-Request-ID`:从 `ctx.Value("RequestID")`(string 类型)读取,用于链路追踪;
|
||||
- `X-Tenant-ID`:优先级为 context 中的 `"TenantID"` 值(支持 `uint64`、`*uint64`、数字字符串)> `WithTenantID` 的客户端级默认值。
|
||||
|
||||
单请求租户覆盖示例:
|
||||
|
||||
```go
|
||||
tenantID := uint64(10000)
|
||||
ctx := context.WithValue(context.Background(), "TenantID", &tenantID)
|
||||
kb, err := apiClient.GetKnowledgeBase(ctx, kbID)
|
||||
```
|
||||
|
||||
注意:JWT 与租户级 API Key 本身已携带租户身份,普通用户**不应**设置 `X-Tenant-ID`(服务端 auth 中间件会对携带该头的 bearer 请求执行跨租户校验,普通用户会得到 403)。
|
||||
|
||||
### Raw 逃生舱
|
||||
|
||||
`Client.Raw(ctx, method, path, body)`(Experimental)以客户端已配置的认证头直接发起任意 HTTP 请求,用于一次性集成与 `weknora api` CLI 透传;有类型化方法时应优先使用类型化方法。
|
||||
|
||||
## 资源与方法总览
|
||||
|
||||
以下均为 `Client` 的公开方法,内部方法(`buildRequest`、`doRequest`、`doRequestStream`、`processAgentSSEStream` 等)不列入。
|
||||
|
||||
### 认证 Auth — `client/auth.go`
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| `Login` | 邮箱密码登录,返回 JWT access/refresh token |
|
||||
| `GetCurrentUser` | 获取当前登录主体与租户信息(`GET /api/v1/auth/me`) |
|
||||
| `RefreshToken` | 用 refresh token 换取新 access token |
|
||||
|
||||
### 知识库 KnowledgeBase — `client/knowledgebase.go`
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| `CreateKnowledgeBase` | 创建知识库 |
|
||||
| `GetKnowledgeBase` | 获取知识库详情 |
|
||||
| `ListKnowledgeBases` | 列出知识库 |
|
||||
| `UpdateKnowledgeBase` | 更新知识库 |
|
||||
| `DeleteKnowledgeBase` | 删除知识库 |
|
||||
| `ClearKnowledgeBaseContents` | 清空知识库内容 |
|
||||
| `HybridSearch` | 在知识库内混合检索(向量 + 关键词);可传 `ResourceURLOptions` 返回文件直链 |
|
||||
| `TogglePinKnowledgeBase` | 置顶/取消置顶 |
|
||||
| `ListMoveTargets` | 列出知识可迁移的目标知识库 |
|
||||
| `CopyKnowledgeBase` | 复制知识库 |
|
||||
| `DuplicateKnowledgeBase` | 复制(duplicate)知识库 |
|
||||
| `GetKBCloneProgress` | 查询克隆任务进度 |
|
||||
|
||||
### 知识 Knowledge — `client/knowledge.go`
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| `CreateKnowledgeFromFile` | 从本地文件上传创建知识(multipart,支持 metadata、多模态开关、自定义文件名、channel、解析配置覆盖) |
|
||||
| `CreateKnowledgeFromURL` | 从 URL 创建知识 |
|
||||
| `GetKnowledge` | 获取知识详情 |
|
||||
| `GetKnowledgeBatch` | 批量获取知识 |
|
||||
| `ListKnowledge` | 分页列出知识 |
|
||||
| `ListKnowledgeWithFilter` | 带过滤条件列出知识 |
|
||||
| `DeleteKnowledge` | 删除知识 |
|
||||
| `DownloadKnowledgeFile` | 下载知识原始文件到本地路径 |
|
||||
| `OpenKnowledgeFile` | 以流方式打开知识原始文件(返回文件名 + `io.ReadCloser`) |
|
||||
| `UpdateKnowledge` | 更新知识 |
|
||||
| `ReparseKnowledge` | 重新解析知识 |
|
||||
| `CancelKnowledgeParse` | 取消解析任务 |
|
||||
| `GetKnowledgeProcessingSpans` | 获取知识处理链路 span |
|
||||
| `UpdateImageInfo` | 更新图片信息 |
|
||||
| `CreateManualKnowledge` | 创建手写(manual)知识 |
|
||||
| `UpdateManualKnowledge` | 更新手写知识 |
|
||||
| `FilterKnowledge` | 按关键词/文件类型/agent 过滤知识 |
|
||||
| `MoveKnowledge` | 跨知识库迁移知识 |
|
||||
| `GetKnowledgeMoveProgress` | 查询迁移任务进度 |
|
||||
| `PreviewKnowledgeFile` | 预览知识文件(返回原始 `*http.Response`) |
|
||||
| `BatchUpdateKnowledgeTags` | 批量更新知识标签 |
|
||||
|
||||
### 分块 Chunk — `client/chunk.go`
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| `ListKnowledgeChunks` | 分页列出某个知识的 chunk |
|
||||
| `UpdateChunk` | 更新 chunk 内容/启用状态 |
|
||||
| `DeleteChunk` | 删除 chunk |
|
||||
| `GetChunkByIDOnly` | 仅凭 chunk ID 获取 chunk |
|
||||
| `DeleteGeneratedQuestion` | 删除 chunk 生成的问题 |
|
||||
| `DeleteChunksByKnowledgeID` | 删除某知识的全部 chunk |
|
||||
|
||||
### 会话 Session — `client/session.go`
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| `CreateSession` | 创建会话 |
|
||||
| `GetSession` | 获取会话 |
|
||||
| `GetSessionsByTenant` | 分页列出租户会话 |
|
||||
| `UpdateSession` | 更新会话 |
|
||||
| `DeleteSession` | 删除会话 |
|
||||
| `BatchDeleteSessions` | 批量删除会话 |
|
||||
| `GenerateTitle` | 生成会话标题 |
|
||||
| `KnowledgeQAStream` | 知识问答(SSE 流式,见下文) |
|
||||
| `ContinueStream` | 续接进行中的流(断线重连场景) |
|
||||
| `StopSession` | 停止某条 assistant 消息的生成 |
|
||||
| `SearchKnowledge` | 知识检索 |
|
||||
|
||||
### 消息 Message — `client/message.go`、`client/message_suggestion.go`
|
||||
|
||||
| 方法 | 说明 | 源文件 |
|
||||
|---|---|---|
|
||||
| `LoadMessages` | 按时间加载消息 | `client/message.go` |
|
||||
| `GetRecentMessages` | 获取最近 N 条消息 | `client/message.go` |
|
||||
| `GetMessagesBefore` | 获取某时间点之前的消息 | `client/message.go` |
|
||||
| `SearchMessages` | 搜索历史消息 | `client/message.go` |
|
||||
| `GetChatHistoryKBStats` | 聊天历史按知识库统计 | `client/message.go` |
|
||||
| `DeleteMessage` | 删除消息 | `client/message.go` |
|
||||
| `EnsureMessageSuggestions` | 确保(可强制重新)生成推荐问题 | `client/message_suggestion.go` |
|
||||
| `GetMessageSuggestions` | 获取消息的推荐问题 | `client/message_suggestion.go` |
|
||||
| `RecordMessageSuggestionEvent` | 上报推荐问题点击/曝光事件 | `client/message_suggestion.go` |
|
||||
|
||||
### Agent 对话(流式)— `client/agent.go`
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| `AgentQAStream` | Agent 模式流式问答(Deprecated,简化入口) |
|
||||
| `AgentQAStreamWithRequest` | Agent 模式流式问答(完整 `AgentQARequest` 载荷) |
|
||||
| `NewAgentSession` | 创建 `AgentSession` 包装器(其上有 `Ask` / `AskWithRequest` / `GetSessionID`) |
|
||||
|
||||
### Agent 管理 — `client/agent_manage.go`
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| `CreateAgent` | 创建自定义 Agent |
|
||||
| `ListAgents` | 列出 Agent |
|
||||
| `GetAgent` | 获取 Agent |
|
||||
| `UpdateAgent` | 更新 Agent |
|
||||
| `DeleteAgent` | 删除 Agent |
|
||||
| `CopyAgent` | 复制 Agent |
|
||||
| `GetAgentPlaceholders` | 获取 Agent 配置占位符 |
|
||||
| `GetSuggestedQuestions` | 获取 Agent 建议问题 |
|
||||
|
||||
### 模型 Model — `client/model.go`
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| `CreateModel` | 创建模型 |
|
||||
| `GetModel` | 获取模型 |
|
||||
| `ListModels` | 列出模型 |
|
||||
| `UpdateModel` | 更新模型 |
|
||||
| `DeleteModel` | 删除模型 |
|
||||
| `ListModelProviders` | 按模型类型列出模型提供商 |
|
||||
|
||||
### 租户 Tenant — `client/tenant.go`
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| `CreateTenant` | 创建租户 |
|
||||
| `GetTenant` | 获取租户 |
|
||||
| `UpdateTenant` | 更新租户 |
|
||||
| `DeleteTenant` | 删除租户 |
|
||||
| `ListTenants` | 列出租户 |
|
||||
| `ListAllTenants` | 列出全部租户(管理员) |
|
||||
| `SearchTenants` | 搜索租户(分页) |
|
||||
| `ListTenantAPIKeys` | 列出租户 API Key |
|
||||
| `CreateTenantAPIKey` | 创建租户 API Key |
|
||||
| `DeleteTenantAPIKey` | 删除租户 API Key |
|
||||
| `GetTenantKV` | 读取租户级 KV 配置 |
|
||||
| `UpdateTenantKV` | 更新租户级 KV 配置 |
|
||||
| `GetAPIPrincipalConfig` | 获取 API 主体配置 |
|
||||
| `UpdateAPIPrincipalConfig` | 更新 API 主体配置 |
|
||||
| `CreateAPIPrincipalTestToken` | 创建 API 主体测试 token |
|
||||
|
||||
### 组织与共享 Organization — `client/organization.go`
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| `CreateOrganization` / `ListMyOrganizations` / `GetOrganization` / `UpdateOrganization` / `DeleteOrganization` | 组织 CRUD |
|
||||
| `SearchOrganizations` / `PreviewOrganizationByInviteCode` | 搜索/邀请码预览组织 |
|
||||
| `JoinOrganizationByInviteCode` / `SubmitJoinRequest` / `JoinByOrganizationID` / `LeaveOrganization` / `RequestRoleUpgrade` | 加入/退出/升级角色 |
|
||||
| `GenerateInviteCode` / `SearchUsersForInvite` / `InviteMember` | 邀请成员 |
|
||||
| `ListOrgMembers` / `UpdateMemberRole` / `RemoveMember` | 成员管理 |
|
||||
| `ListJoinRequests` / `ReviewJoinRequest` | 加入申请审批 |
|
||||
| `ShareKnowledgeBase` / `ListKBShares` / `UpdateSharePermission` / `RemoveKBShare` | 知识库共享 |
|
||||
| `ShareAgent` / `ListAgentShares` / `RemoveAgentShare` | Agent 共享 |
|
||||
| `ListOrgShares` / `ListOrgAgentShares` / `ListSharedKnowledgeBases` / `ListSharedAgents` | 共享资源查询 |
|
||||
|
||||
### FAQ — `client/faq.go`
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| `ListFAQEntries` | 分页列出 FAQ 条目 |
|
||||
| `UpsertFAQEntries` | 批量新增/更新 FAQ 条目 |
|
||||
| `CreateFAQEntry` | 创建单条 FAQ |
|
||||
| `GetFAQEntry` | 获取单条 FAQ |
|
||||
| `UpdateFAQEntry` | 更新单条 FAQ |
|
||||
| `AddSimilarQuestions` | 追加相似问 |
|
||||
| `UpdateFAQEntryFieldsBatch` | 批量更新字段 |
|
||||
| `UpdateFAQEntryTagBatch` | 批量更新标签 |
|
||||
| `DeleteFAQEntries` | 批量删除 |
|
||||
| `SearchFAQEntries` | 检索 FAQ |
|
||||
| `ExportFAQEntries` | 导出为 CSV(返回 `[]byte`) |
|
||||
| `GetFAQImportProgress` | 查询异步导入任务进度(含 dry run) |
|
||||
| `UpdateLastFAQImportResultDisplayStatus` | 更新最近导入结果的展示状态 |
|
||||
|
||||
### 标签 Tag — `client/tag.go`
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| `ListTags` | 列出标签 |
|
||||
| `CreateTag` | 创建标签 |
|
||||
| `UpdateTag` / `UpdateTagBySeqID` | 更新标签(按 ID / 按 seq ID) |
|
||||
| `DeleteTag` / `DeleteTagBySeqID` | 删除标签(按 ID / 按 seq ID) |
|
||||
|
||||
### MCP 服务 — `client/mcp_service.go`
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| `CreateMCPService` / `ListMCPServices` / `GetMCPService` / `UpdateMCPService` / `DeleteMCPService` | MCP 服务 CRUD |
|
||||
| `TestMCPService` | 连通性测试 |
|
||||
| `GetMCPServiceTools` / `GetMCPServiceResources` | 列出 MCP 工具/资源 |
|
||||
| `ResolveToolApproval` | 处理工具调用审批 |
|
||||
|
||||
### 初始化与模型检测 — `client/initialization.go`
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| `GetInitializationConfig` / `InitializeByKB` / `UpdateKBConfig` / `SetKBModelConfig` | 知识库初始化与模型配置 |
|
||||
| `CheckOllamaStatus` / `ListOllamaModels` / `CheckOllamaModels` | Ollama 状态与模型探测 |
|
||||
| `DownloadOllamaModel` / `GetOllamaDownloadProgress` / `ListOllamaDownloadTasks` | Ollama 模型下载任务 |
|
||||
| `CheckRemoteModel` / `TestEmbeddingModel` / `CheckRerankModel` / `TestMultimodalFunction` | 远程 LLM / Embedding / Rerank / 多模态连通性检测 |
|
||||
| `ExtractTextRelations` | 文本关系抽取测试 |
|
||||
|
||||
### 系统 System — `client/system.go`
|
||||
|
||||
| 方法 | 说明 |
|
||||
|---|---|
|
||||
| `GetSystemInfo` | 获取系统信息(版本等) |
|
||||
| `ListParserEngines` / `CheckParserEngines` | 文档解析引擎列表/检测 |
|
||||
| `ReconnectDocReader` | 重连 DocReader 服务 |
|
||||
| `GetStorageEngineStatus` / `CheckStorageEngine` | 存储引擎状态/检测 |
|
||||
|
||||
### 其他
|
||||
|
||||
| 方法 | 说明 | 源文件 |
|
||||
|---|---|---|
|
||||
| `StartEvaluation` / `GetEvaluationResult` | 发起评估任务 / 查询评估结果 | `client/evaluation.go` |
|
||||
| `ListSkills` | 列出预置 Agent skill | `client/skill.go` |
|
||||
| `GetWebSearchProviders` | 列出可用 Web 搜索提供商 | `client/web_search.go` |
|
||||
| `Raw` | 原始 HTTP 逃生舱(Experimental) | `client/client.go` |
|
||||
|
||||
合计约 170 个公开方法,覆盖约 20 类资源。
|
||||
|
||||
## 流式对话(SSE)
|
||||
|
||||
SDK 的流式接口采用**回调(callback)机制**而非 channel:SDK 内部用 `bufio.Scanner` 逐行解析 SSE(`event:` / `data:` 前缀,空行分帧),每解析出一帧就调用一次回调;回调返回非 nil error 即中止流。SSE 行缓冲上限被提升到 4 MiB(`scanner.Buffer(make([]byte, 0, 64*1024), 4*1024*1024)`),避免 references 大帧触发 "token too long"。
|
||||
|
||||
流式请求走 `doRequestStream`(`client/client.go`),默认不受 30 秒超时约束,流生命周期由传入的 `ctx` 控制。
|
||||
|
||||
### 知识问答流:`KnowledgeQAStream`(`client/session.go`)
|
||||
|
||||
```go
|
||||
func (c *Client) KnowledgeQAStream(
|
||||
ctx context.Context,
|
||||
sessionID string,
|
||||
request *KnowledgeQARequest,
|
||||
callback func(*StreamResponse) error,
|
||||
) error
|
||||
```
|
||||
|
||||
每帧 `StreamResponse` 携带 `ResponseType`(`answer`、`references`、`thinking`、`tool_call`、`tool_result`、`error`、`reflection`、`session_title`、`agent_query`、`complete`)、增量 `Content`、结束标记 `Done`,以及 `Done` 帧上的 `KnowledgeReferences`(引用来源)。
|
||||
|
||||
### Agent 问答流:`AgentQAStreamWithRequest`(`client/agent.go`)
|
||||
|
||||
```go
|
||||
type AgentEventCallback func(*AgentStreamResponse) error
|
||||
|
||||
func (c *Client) AgentQAStreamWithRequest(ctx context.Context,
|
||||
sessionID string, request *AgentQARequest, callback AgentEventCallback,
|
||||
) error
|
||||
```
|
||||
|
||||
`AgentQARequest` 支持 `KnowledgeBaseIDs`、`AgentID`、`WebSearchEnabled`、`MentionedItems`(@提及知识库/文件/标签/MCP/skill)、`Images`(多模态图片)等字段。也可用便捷包装器:
|
||||
|
||||
```go
|
||||
as := apiClient.NewAgentSession(session.ID)
|
||||
err := as.Ask(ctx, "介绍一下 WeKnora", func(ev *client.AgentStreamResponse) error {
|
||||
if ev.ResponseType == client.AgentResponseTypeAnswer {
|
||||
fmt.Print(ev.Content)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
```
|
||||
|
||||
### 断线续接:`ContinueStream`(`client/session.go`)
|
||||
|
||||
`ContinueStream(ctx, sessionID, messageID, callback)` 以 `GET /api/v1/sessions/continue-stream/{sessionID}?message_id=...` 续接服务端仍在生成的流,回调机制与 `KnowledgeQAStream` 相同;配合 `StopSession(ctx, sessionID, messageID)` 可中止生成。
|
||||
|
||||
## 错误处理
|
||||
|
||||
### HTTP 层:`APIError`(`client/client.go`)
|
||||
|
||||
所有非 2xx 响应被封装为 `*APIError`,用 `errors.As` 按 HTTP 状态码或服务端结构化错误码分支:
|
||||
|
||||
```go
|
||||
var apiErr *client.APIError
|
||||
if errors.As(err, &apiErr) {
|
||||
switch {
|
||||
case apiErr.StatusCode == 404:
|
||||
// 资源不存在
|
||||
case apiErr.Code == client.ServerErrUnauthorized: // 1001
|
||||
// 触发重新登录
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`Code` 为响应体 `{"code":N}` 中的结构化错误码,包内提供常量 `ServerErrBadRequest`(1000) 至 `ServerErrValidation`(1010)。`Error()` 保持 `"HTTP error <status>: <body>"` 的旧格式以兼容字符串匹配的消费者。
|
||||
|
||||
### 流层:`SSEStreamError`(`client/stream_errors.go`)
|
||||
|
||||
当服务端在 SSE 流上发出终止错误帧(`response_type=error, done=true`)时,SDK 会**先把该帧交给回调**,然后返回 `*SSEStreamError`:
|
||||
|
||||
```go
|
||||
type SSEStreamError struct {
|
||||
Content string // 错误帧内容
|
||||
}
|
||||
```
|
||||
|
||||
判断方式(两者等价,推荐前者):
|
||||
|
||||
```go
|
||||
// 方式一:哨兵错误(SSEStreamError.Unwrap() 返回它)
|
||||
if errors.Is(err, client.ErrSSEStreamTerminal) { ... }
|
||||
|
||||
// 方式二:辅助函数(兼容旧版 fmt.Errorf("SSE stream error: ...") 链)
|
||||
if client.IsSSEStreamError(err) { ... }
|
||||
```
|
||||
|
||||
## 日志与链路追踪
|
||||
|
||||
`client/log.go` 提供基于 `log/slog` 的 SDK 内部调试日志,默认写入 `io.Discard`(对使用方完全静默)。嵌入方可在启动时调用:
|
||||
|
||||
```go
|
||||
client.SetDebugLevel("debug") // "debug"/"info"/"warn";其他值(含 "error"、"")静默
|
||||
```
|
||||
|
||||
日志输出到 stderr,包含 SSE 逐行解析、请求失败等 trace 信息。该函数**非并发安全**,须在任何 SDK 调用发起前调用一次。
|
||||
|
||||
链路追踪方面,在 context 中放入 `"RequestID"`(string),SDK 会自动作为 `X-Request-ID` 请求头发送(见 `client/client.go` 的 `applyAuthHeaders`):
|
||||
|
||||
```go
|
||||
ctx := context.WithValue(context.Background(), "RequestID", "req-20260727-0001")
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
以下示例改编自 `client/example.go` 中的真实代码。
|
||||
|
||||
### 示例一:创建知识库并上传文件
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Tencent/WeKnora/client"
|
||||
)
|
||||
|
||||
func main() {
|
||||
apiClient := client.NewClient(
|
||||
"http://localhost:8080",
|
||||
client.WithAPIKey("your-api-key"),
|
||||
client.WithTimeout(30*time.Second),
|
||||
)
|
||||
|
||||
// 创建知识库
|
||||
kb := &client.KnowledgeBase{
|
||||
Name: "Test Knowledge Base",
|
||||
Description: "This is a test knowledge base",
|
||||
ChunkingConfig: client.ChunkingConfig{
|
||||
ChunkSize: 500,
|
||||
ChunkOverlap: 50,
|
||||
Separators: []string{"\n\n", "\n", ". ", "? ", "! "},
|
||||
},
|
||||
EmbeddingModelID: "embedding_model_id",
|
||||
SummaryModelID: "summary_model_id",
|
||||
}
|
||||
createdKB, err := apiClient.CreateKnowledgeBase(context.Background(), kb)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to create knowledge base: %v\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Knowledge base created: ID=%s, Name=%s\n", createdKB.ID, createdKB.Name)
|
||||
|
||||
// 上传文件创建知识
|
||||
metadata := map[string]string{"source": "local", "type": "document"}
|
||||
knowledge, err := apiClient.CreateKnowledgeFromFile(
|
||||
context.Background(), createdKB.ID, "path/to/sample.pdf",
|
||||
metadata, nil, "", "", nil)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to upload knowledge file: %v\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("File uploaded: Knowledge ID=%s, Title=%s\n", knowledge.ID, knowledge.Title)
|
||||
}
|
||||
```
|
||||
|
||||
### 示例二:创建会话并进行流式知识问答
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Tencent/WeKnora/client"
|
||||
)
|
||||
|
||||
func main() {
|
||||
apiClient := client.NewClient("http://localhost:8080",
|
||||
client.WithAPIKey("your-api-key"))
|
||||
|
||||
// 创建会话
|
||||
session, err := apiClient.CreateSession(context.Background(), &client.CreateSessionRequest{
|
||||
Title: "Test Session",
|
||||
Description: "A test session for knowledge Q&A",
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to create session: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 流式问答:累积答案与引用
|
||||
question := "What is artificial intelligence?"
|
||||
var answer strings.Builder
|
||||
var references []*client.SearchResult
|
||||
|
||||
err = apiClient.KnowledgeQAStream(context.Background(),
|
||||
session.ID,
|
||||
&client.KnowledgeQARequest{Query: question},
|
||||
func(response *client.StreamResponse) error {
|
||||
if response.ResponseType == client.ResponseTypeAnswer {
|
||||
answer.WriteString(response.Content)
|
||||
}
|
||||
if response.Done && len(response.KnowledgeReferences) > 0 {
|
||||
references = response.KnowledgeReferences
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
// 区分 SSE 终止错误帧与其他错误
|
||||
if errors.Is(err, client.ErrSSEStreamTerminal) {
|
||||
fmt.Printf("Stream terminated by server error: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("Q&A failed: %v\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
fmt.Printf("Answer: %s\n", answer.String())
|
||||
for i, ref := range references {
|
||||
fmt.Printf("Reference %d: %s\n", i+1, ref.Content)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 示例三:历史消息与 Chunk 管理及资源清理
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/Tencent/WeKnora/client"
|
||||
)
|
||||
|
||||
func main() {
|
||||
apiClient := client.NewClient("http://localhost:8080",
|
||||
client.WithAPIKey("your-api-key"))
|
||||
ctx := context.Background()
|
||||
|
||||
// 获取最近 10 条会话消息
|
||||
sessionID := "your-session-id"
|
||||
messages, err := apiClient.GetRecentMessages(ctx, sessionID, 10)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to get session messages: %v\n", err)
|
||||
} else {
|
||||
for i, msg := range messages {
|
||||
fmt.Printf("%d. Role: %s, Content: %s\n", i+1, msg.Role, msg.Content)
|
||||
}
|
||||
}
|
||||
|
||||
// 管理知识 chunk:分页列出并更新第一个
|
||||
knowledgeID := "your-knowledge-id"
|
||||
chunks, total, err := apiClient.ListKnowledgeChunks(ctx, knowledgeID, 1, 10)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to get knowledge chunks: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("Knowledge has %d chunks, retrieved %d\n", total, len(chunks))
|
||||
if len(chunks) > 0 {
|
||||
updated, err := apiClient.UpdateChunk(ctx, knowledgeID, chunks[0].ID,
|
||||
&client.UpdateChunkRequest{
|
||||
Content: "Updated chunk content - " + chunks[0].Content,
|
||||
IsEnabled: true,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to update chunk: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("Chunk updated: ID=%s\n", updated.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 清理资源
|
||||
if err := apiClient.DeleteSession(ctx, sessionID); err != nil {
|
||||
fmt.Printf("Failed to delete session: %v\n", err)
|
||||
}
|
||||
if err := apiClient.DeleteKnowledge(ctx, knowledgeID); err != nil {
|
||||
fmt.Printf("Failed to delete knowledge: %v\n", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 参考源码
|
||||
|
||||
- 客户端核心与错误类型:`client/client.go`
|
||||
- 认证:`client/auth.go`
|
||||
- 流式问答:`client/session.go`、`client/agent.go`
|
||||
- 流式错误:`client/stream_errors.go`
|
||||
- 日志:`client/log.go`
|
||||
- 完整用法示例:`client/example.go`
|
||||
@@ -0,0 +1,101 @@
|
||||
# 微信小程序客户端
|
||||
|
||||
WeKnora 在仓库的 `miniprogram/` 目录下提供了一个轻量级的微信小程序客户端,作为移动端的快捷入口。它不试图复刻 Web 前端的完整功能,而是聚焦三件事:
|
||||
|
||||
- 配置 WeKnora API 地址与租户 API Key;
|
||||
- 列出并选择知识库(Knowledge Base),把网页 URL 导入到选中的知识库;
|
||||
- 面向选中的知识库发起知识问答(Knowledge Chat)。
|
||||
|
||||
## 技术栈
|
||||
|
||||
该客户端是**原生微信小程序**(native Mini Program),未使用 Taro / uni-app / mpvue 等跨端框架,也没有任何 npm 运行时依赖:
|
||||
|
||||
- `miniprogram/app.js` — 标准的 `App({...})` 入口,`onLaunch` 时向本地存储写入默认设置;
|
||||
- `miniprogram/app.json` — 标准小程序全局配置(`pages`、`window`、`tabBar`);
|
||||
- `miniprogram/app.wxss` — 全局样式;页面均为 `js / wxml / wxss / json` 四件套;
|
||||
- `miniprogram/package.json` — 包名 `weknora-miniprogram`(version `0.1.0`),`description` 为 "WeChat Mini Program plugin for WeKnora",**没有 `dependencies`**,仅有一个测试脚本(见下文「测试」);
|
||||
- `miniprogram/project.config.json` — `compileType: "miniprogram"`,`libVersion: "latest"`(基础库使用最新版),编译选项开启 `es6`、`enhance`、`postcss`、`minified`,并开启 `urlCheck: true`(合法域名校验)。**注意:该文件刻意不包含 `appid` 字段**,AppID 通过私有配置文件提供(见「构建与发布」)。
|
||||
|
||||
全局窗口样式:导航栏标题 `WeKnora`,背景色 `#0d3b2a`(深绿),文字白色。
|
||||
|
||||
## 页面清单
|
||||
|
||||
`miniprogram/app.json` 中注册了 3 个页面,且三者同时构成底部 `tabBar`(选中色 `#07c05f`):
|
||||
|
||||
| 页面路径 | 名称(tabBar 文案) | 功能 |
|
||||
| --- | --- | --- |
|
||||
| `pages/index/index` | Knowledge(知识库) | 首页。检测是否已配置 baseUrl / API Key,未配置时提示并可一键跳转 Settings;调用 `GET /api/v1/knowledge-bases` 加载知识库列表,通过 `picker` 或列表点选知识库(选择结果持久化到本地存储);输入网页 URL 后调用 `POST /api/v1/knowledge-bases/{id}/knowledge/url` 将该 URL 导入选中知识库(`enable_multimodel` 固定为 `false`) |
|
||||
| `pages/chat/chat` | Chat(问答) | 知识问答页。首次提问时通过 `POST /api/v1/sessions` 懒创建会话(携带选中的 `knowledge_base_id`),随后调用 `POST /api/v1/knowledge-chat/{sessionId}` 提问;返回体为 SSE 文本,客户端用 `utils/sse.js` 解析并拼接 `response_type === "answer"` 的分片后整体展示(解析失败则回退展示原始响应) |
|
||||
| `pages/settings/settings` | Settings(设置) | 连接配置页。填写 API Base URL 与 API Key(密码输入框),保存到本地存储 `weknora_settings` |
|
||||
|
||||
## 后端地址与认证配置
|
||||
|
||||
小程序**不在代码中硬编码后端地址**,一切连接信息由用户在「Settings」页填写,存储于 `wx.setStorageSync` 的本地存储键 `weknora_settings` 中,结构包含三个字段:
|
||||
|
||||
```js
|
||||
{
|
||||
baseUrl: "http://localhost:8080", // app.js onLaunch 写入的默认值
|
||||
apiKey: "",
|
||||
selectedKnowledgeBaseId: ""
|
||||
}
|
||||
```
|
||||
|
||||
- **默认值**:`miniprogram/app.js` 在 `onLaunch` 中若发现本地无设置,会写入默认 `baseUrl: "http://localhost:8080"`、空 `apiKey`。默认值仅便于本地开发,实际使用必须在 Settings 页改为真实地址。
|
||||
- **读写与规范化**:`miniprogram/utils/config.js` 提供 `getSettings()` / `saveSettings()`,并通过 `normalizeBaseUrl()` 去除首尾空白与末尾 `/`。
|
||||
- **认证方式为 API Key**:`miniprogram/utils/request.js` 中所有请求统一携带请求头:
|
||||
- `X-API-Key: <用户填写的 API Key>`(来自 WeKnora 租户设置页,形如 `sk-...`);
|
||||
- `X-Request-ID: mp-<时间戳>-<随机串>`(便于服务端追踪);
|
||||
- `Content-Type: application/json`。
|
||||
- **前置校验**:`baseUrl` 或 `apiKey` 任一缺失时,请求会直接以错误 Promise 拒绝("Please configure the WeKnora API base URL / API key first.");`pages/index/index.js` 的 `onShow` 也会据此显示引导用户去 Settings 页的提示。
|
||||
- **AppID 配置**:微信小程序 AppID 不放在共享的 `project.config.json` 中,而是复制 `miniprogram/project.private.config.json.example` 为 `project.private.config.json` 并填入真实 AppID(示例文件内容为 `{"appid": "your-wechat-mini-program-appid"}`)。
|
||||
|
||||
调用到的后端接口(均定义在 `miniprogram/utils/request.js`):
|
||||
|
||||
| 函数 | 方法与路径 |
|
||||
| --- | --- |
|
||||
| `listKnowledgeBases()` | `GET /api/v1/knowledge-bases` |
|
||||
| `createKnowledgeFromURL(kbId, url, enableMultimodel)` | `POST /api/v1/knowledge-bases/{kbId}/knowledge/url` |
|
||||
| `createSession(kbId)` | `POST /api/v1/sessions` |
|
||||
| `knowledgeChat(sessionId, query, kbId)` | `POST /api/v1/knowledge-chat/{sessionId}` |
|
||||
|
||||
## utils/ 工具模块
|
||||
|
||||
| 文件 | 职责 |
|
||||
| --- | --- |
|
||||
| `miniprogram/utils/config.js` | 设置的持久化层:定义存储键 `STORAGE_KEY = "weknora_settings"`,提供 `getSettings()`、`saveSettings()`(合并式更新)与 `normalizeBaseUrl()`(trim 并去除末尾斜杠) |
|
||||
| `miniprogram/utils/request.js` | 基于 `wx.request` 的 Promise 化 HTTP 封装:拼接 `baseUrl + path`、注入 `X-API-Key` / `X-Request-ID` 头、统一 2xx 判定与错误消息提取(优先 `error.message`,其次 `message`,兜底 `HTTP <status>`);并导出上表 4 个业务 API 函数 |
|
||||
| `miniprogram/utils/sse.js` | Server-Sent Events 文本解析器:`parseSSE(raw)` 按空行切分事件块、解析 `event:` / `data:` 行;`collectAnswerFromSSE(raw)` 将各事件的 `data` 按 JSON 解析并累加 `response_type === "answer"` 的 `content`,得到最终答案文本。注意小程序端**不做流式渲染**,而是等 `wx.request` 拿到完整 SSE 文本后一次性解析展示 |
|
||||
|
||||
## 数据流概览
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
S["Settings 页<br/>(baseUrl + API Key)"] -->|"wx.setStorageSync(weknora_settings)"| C["utils/config.js"]
|
||||
K["Knowledge 页<br/>(pages/index)"] -->|"listKnowledgeBases / createKnowledgeFromURL"| R["utils/request.js<br/>(X-API-Key 头)"]
|
||||
Q["Chat 页<br/>(pages/chat)"] -->|"createSession / knowledgeChat"| R
|
||||
R -->|"wx.request"| B["WeKnora 后端<br/>/api/v1/*"]
|
||||
B -->|"SSE 文本"| P["utils/sse.js<br/>collectAnswerFromSSE"]
|
||||
P --> Q
|
||||
C --> R
|
||||
```
|
||||
|
||||
## 构建与发布流程
|
||||
|
||||
小程序无需编译步骤(原生开发、无构建工具链),直接用微信开发者工具(WeChat DevTools)打开即可:
|
||||
|
||||
1. **导入项目**:在微信开发者工具中选择「导入项目」,目录指向仓库的 `miniprogram/`。工具会读取 `project.config.json`(项目名 "WeKnora Mini Program")。
|
||||
2. **配置 AppID**:复制 `miniprogram/project.private.config.json.example` 为 `project.private.config.json`,将 `appid` 替换为你自己的小程序 AppID。共享的 `project.config.json` 刻意不含 AppID,避免维护者被迫使用占位项目;`project.private.config.json` 属于个人私有配置,不应提交。
|
||||
3. **配置后端连接**:运行后进入 **Settings** tab,填写 API Base URL(如 `https://weknora.example.com`)与从 WeKnora 租户设置页获取的 API Key,保存。
|
||||
4. **本地调试注意**:`project.config.json` 开启了 `urlCheck: true`,开发者工具默认会拦截 `localhost` 等非合法域名请求。本地测试可在 DevTools 中勾选「不校验合法域名」,或通过 HTTPS 开发域名暴露 WeKnora 服务。
|
||||
5. **发布**:正式发布前,需在微信公众平台的小程序管理后台,把 WeKnora API 域名(必须为 HTTPS)加入 request 合法域名(request 域名白名单);随后在开发者工具中点击「上传」提交代码,再在管理后台提交审核并发布。
|
||||
|
||||
### 测试
|
||||
|
||||
`miniprogram/package.json` 定义了唯一脚本:
|
||||
|
||||
```bash
|
||||
cd miniprogram
|
||||
npm test # 实际执行 node --test ../tests/miniprogram/*.test.js
|
||||
```
|
||||
|
||||
即使用 Node.js 内置 test runner 运行仓库 `tests/miniprogram/miniprogram.test.js` 中的单元测试(覆盖 `utils/` 下的纯函数逻辑),无需安装任何依赖。
|
||||
@@ -0,0 +1,172 @@
|
||||
# 桌面客户端(WeKnora Lite Desktop)
|
||||
|
||||
::: warning 尚未正式发布
|
||||
桌面应用目前没有随 Release 提供安装包,需按[安装部署](../01-getting-started/02-installation.md)自行构建。
|
||||
:::
|
||||
|
||||
WeKnora 提供基于 [Wails v2](https://wails.io) 的跨平台桌面应用「WeKnora Lite」,源码位于 `cmd/desktop/`。它在桌面进程内运行完整的 WeKnora 后端(Gin 服务),配合 SQLite(`sqlite_fts5`)与本地文件存储,双击启动即可使用,无需 Docker、无需外部数据库。检索、问答、知识库管理等能力与[单二进制 Lite](../01-getting-started/02-installation.md) 一致,本篇聚焦桌面形态特有的部分:窗口与生命周期、数据目录、端口与局域网绑定、更新检查。
|
||||
|
||||
## 1. 总体架构
|
||||
|
||||
桌面应用由三部分组成(均在同一进程内):
|
||||
|
||||
1. **内嵌后端**:`cmd/desktop/main.go` 中通过 `container.BuildContainer()` 构建与服务器版相同的依赖注入容器,在独立 goroutine 中启动 `http.Server`(Gin router)。
|
||||
2. **Wails 窗口(WebView)**:`wails.Run()` 创建原生窗口,前端页面通过 `assetserver.Options.Handler` 挂载的 **反向代理**(`httputil.NewSingleHostReverseProxy`)转发到内嵌后端,因此 WebView 加载的就是后端 `./web` 目录提供的 SPA。
|
||||
3. **Go 绑定层**:`cmd/desktop/app.go` 中的 `App` 结构体通过 `Bind` 暴露给前端 JS(`window.go.main.App.*`)。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph D["WeKnora Lite 桌面进程"]
|
||||
W["Wails WebView (前端 SPA)"]
|
||||
P["Reverse Proxy (assetserver)"]
|
||||
B["内嵌 Gin 后端 (127.0.0.1:随机或固定端口)"]
|
||||
S["SQLite + 本地文件存储 (Application Support)"]
|
||||
W --> P --> B --> S
|
||||
W -- "window.go.main.App.* 绑定" --> A["App 结构体 (app.go)"]
|
||||
end
|
||||
B -. "可选 0.0.0.0 监听" .-> L["局域网其他设备 (LAN API)"]
|
||||
```
|
||||
|
||||
### 端口与监听
|
||||
|
||||
由 `main.go` 中的 `desktopBackendListenAddr()` 决定:
|
||||
|
||||
- 默认绑定 `127.0.0.1`,端口取 `desktop-prefs.json` 中保存的 `http_port`;未设置(为 0)时使用 `:0` 随机空闲端口,并带指数退避重试(`listenWithRetry`,最多 10 次)。
|
||||
- 若偏好项 `http_bind_public` 为 `true`,则改为监听 `0.0.0.0`,并通过 `desktopPreferredLANIPv4()` 探测一个非回环 IPv4(优先私网地址),拼出 `http://<LAN-IP>:<port>/api/v1` 供局域网内其他设备调用。
|
||||
- 反向代理与 WebView 的 API 调用始终走回环地址 `http://127.0.0.1:<port>`,不会以 `0.0.0.0` 作为拨号目标。
|
||||
|
||||
### 数据存储位置(macOS .app 运行时)
|
||||
|
||||
`main.go` 的 `configureDesktopStorage()` 在检测到从 `.app/Contents/MacOS` 运行时:
|
||||
|
||||
- 数据目录定为 `~/Library/Application Support/WeKnora Lite/`(名称取自 .app bundle 名)。
|
||||
- SQLite 数据库:`.../data/weknora.db`(通过设置 `DB_PATH` 环境变量注入)。
|
||||
- 本地文件存储:`.../data/files`(`LOCAL_STORAGE_BASE_DIR`)。
|
||||
- `migrateLegacyDesktopData()` 会把旧版存放在 `.app/Contents/Resources/data` 里的数据一次性迁移到 Application Support。
|
||||
- 工作目录会切到 `.app/Contents/Resources`,以便读取打包进去的 `config/config.yaml`、`.env`、`migrations/sqlite` 与 `web/` 前端资源。
|
||||
|
||||
## 2. 主要源码文件
|
||||
|
||||
| 文件 | 作用 |
|
||||
|------|------|
|
||||
| `cmd/desktop/main.go` | 主入口(`//go:build !bindings`):启动内嵌 Gin 后端、构建 macOS 菜单、配置 Wails 窗口与反向代理、注入 DomReady JS |
|
||||
| `cmd/desktop/main_bindings.go` | 绑定生成入口(`//go:build bindings`):`wails build` 生成前端绑定阶段用 `-tags bindings` 单独编译,只 `Bind` 不启动 Gin/数据库 |
|
||||
| `cmd/desktop/app.go` | `App` 结构体与全部 Wails 绑定方法 |
|
||||
| `cmd/desktop/prefs.go` | 桌面偏好设置的读写(`desktop-prefs.json`) |
|
||||
| `cmd/desktop/update.go` | 基于 GitHub Releases 的检查更新 / 下载 / 安装重启逻辑 |
|
||||
| `cmd/desktop/wails.json` | Wails 构建配置 |
|
||||
| `cmd/desktop/build/` | 打包资源:`appicon.png`(应用图标)、`darwin/Info.plist`(macOS bundle 模板) |
|
||||
|
||||
## 3. 窗口配置与前端注入
|
||||
|
||||
`wails.Run(&options.App{...})` 的关键配置(见 `cmd/desktop/main.go`):
|
||||
|
||||
- 标题 `WeKnora Lite`,初始尺寸 **1280 × 800**,可调整大小,启动即显示。
|
||||
- `AssetServer.Handler` 使用反向代理指向内嵌后端 —— **前端资源并非 Go embed,而是后端 `./web` 目录(打包在 `.app/Contents/Resources/web`)提供的 SPA**。
|
||||
- macOS 专属:`mac.TitleBarHiddenInset()` 隐藏式标题栏,WebView 不透明。
|
||||
- 应用菜单:`About WeKnora`(含 "Open GitHub" 按钮,指向 `https://github.com/Tencent/WeKnora`)、`Check for Updates...`、`Quit`(Cmd+Q)、标准 Edit 菜单、`View > Reload`(Cmd+R,向前端发送 `app:reload` 事件)。
|
||||
|
||||
`OnDomReady` 时向 WebView 注入三段 JS:
|
||||
|
||||
1. `wailsThemeSyncJS`:按 `localStorage` 的 `WeKnora_theme` 同步深浅色主题与窗口背景色。
|
||||
2. `dragHandlerJS`:自定义窗口拖拽处理(绕过 Wails 的 CSS 变量拖拽检测,改用 `el.closest()` DOM 遍历 + 顶部 38px 标题栏区域判定,通过 WKWebView 消息桥发送 `drag`);同时拦截外部 `http(s)` 链接与 `window.open`,改用系统浏览器打开(`BrowserOpenURL`)。
|
||||
3. 注入 `window.__WEKNORA_API_BASE__`(真实 API 根路径 `http://127.0.0.1:<port>/api/v1`)以及可选的 `window.__WEKNORA_API_LAN_BASE__`(LAN 访问地址)。
|
||||
|
||||
## 4. Wails 绑定方法(前端可调用)
|
||||
|
||||
`App` 结构体(`cmd/desktop/app.go`)通过 `Bind` 暴露,前端以 `window.go.main.App.<方法名>` 调用,生成的 TypeScript 绑定位于 `frontend/src/wailsjs/go/main/App.d.ts`:
|
||||
|
||||
| 方法 | 签名(JS 侧) | 说明 |
|
||||
|------|--------------|------|
|
||||
| `GetAPIBaseURL` | `(): Promise<string>` | 返回本地 REST API 根地址,如 `http://127.0.0.1:PORT/api/v1`(WebView 的 `window.location.origin` 不是 API 主机,需用此值) |
|
||||
| `GetAPILanBaseURL` | `(): Promise<string>` | 返回建议给局域网其他设备使用的 API 地址(`…/api/v1`);非 bind-public 模式或 IP 探测失败时为空 |
|
||||
| `GetDesktopHTTPPortSetting` | `(): Promise<number>` | 读取已保存的本地 API 端口偏好(0 = 每次启动随机端口) |
|
||||
| `SetDesktopHTTPPortSetting` | `(port: number): Promise<void>` | 保存端口偏好;需重启应用生效 |
|
||||
| `GetDesktopHTTPBindPublicSetting` | `(): Promise<boolean>` | 读取是否监听所有网卡(`0.0.0.0`)的偏好 |
|
||||
| `SetDesktopHTTPBindPublicSetting` | `(v: boolean): Promise<void>` | 保存 LAN/公开监听偏好;需重启应用生效 |
|
||||
| `GetDesktopListenPublicActive` | `(): Promise<boolean>` | 当前会话是否**实际**在所有网卡上监听(运行时状态,而非保存的偏好) |
|
||||
| `CheckForUpdates` | `(): Promise<void>` | 手动触发更新检查(有"已是最新"等对话框反馈) |
|
||||
| `AutoCheckForUpdates` | `(): Promise<void>` | 静默检查更新并自动后台下载 |
|
||||
|
||||
## 5. 偏好设置存储(cmd/desktop/prefs.go)
|
||||
|
||||
偏好保存为 JSON 文件 `desktop-prefs.json`,路径为 `os.UserConfigDir()/WeKnora Lite/desktop-prefs.json`:
|
||||
|
||||
- macOS:`~/Library/Application Support/WeKnora Lite/desktop-prefs.json`
|
||||
- Windows:`%AppData%\WeKnora Lite\desktop-prefs.json`
|
||||
- Linux:`~/.config/WeKnora Lite/desktop-prefs.json`
|
||||
|
||||
文件权限 `0600`,字段如下:
|
||||
|
||||
| 字段 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `http_port` | int | 0 | 内嵌 API 服务监听端口;0 或非法值(超出 1–65535)表示每次启动使用随机空闲端口 |
|
||||
| `http_bind_public` | bool | false | 是否监听 `0.0.0.0`(允许局域网/公网访问内嵌 API) |
|
||||
|
||||
读写入口:`LoadDesktopPrefsHTTPPort()` / `LoadDesktopHTTPBindPublic()` / `SaveDesktopHTTPPortPreference()` / `SaveDesktopHTTPBindPublicPreference()`,读取失败或解析失败时静默回退为零值。
|
||||
|
||||
## 6. 自动更新机制(cmd/desktop/update.go)
|
||||
|
||||
`checkUpdate(ctx, currentVersion, showUpToDate, autoDownload)` 在 goroutine 中执行:
|
||||
|
||||
1. **版本来源**:`desktopAboutVersion()` 优先使用构建时 ldflags 注入的 `handler.Version`,否则向上查找仓库根目录的 `VERSION` 文件;无法确定版本时放弃检查。
|
||||
2. **检查**:GET `https://api.github.com/repos/Tencent/WeKnora/releases/latest`(超时 10s,带 `User-Agent: WeKnora-Lite-Desktop-App`;若设置了环境变量 `GITHUB_TOKEN` 则附带 `Authorization` 头以提升速率限制)。用 `golang.org/x/mod/semver` 比较 `tag_name` 与当前版本。
|
||||
3. **选择资产**:`findBestAsset()` 按 `runtime.GOOS/GOARCH` 匹配 release assets 文件名——OS 关键词(`mac`/`win`/`linux`)+ 架构关键词(`amd64`/`arm64`,兼容 `universal`/`aarch64`),逐级回退:OS+Arch → 仅 OS → macOS 的 `.dmg` → Windows 的 `.exe`;均无匹配时打开 release 页面。
|
||||
4. **下载**:`downloadAndInstall()` 下载到系统临时目录;`autoDownload` 模式静默下载,手动模式先弹 "Update Available" 对话框。下载完成后询问 "Restart Now / Later"。
|
||||
5. **安装与重启**(`applyUpdateAndRestart()`,平台差异):
|
||||
- **Windows**:写临时 `weknora_update.bat`(延时 2 秒 → 静默运行安装包 `/S` → 重启原程序 → 自删除),`cmd.exe /C start /b` 执行后退出应用。
|
||||
- **macOS(.dmg)**:`hdiutil attach` 挂载到临时挂载点,找到其中的 `.app`,写临时 `weknora_update.sh`:`rm -rf` 旧 bundle 并 `cp -a` 新 bundle(失败时通过 `osascript … with administrator privileges` 提权重试)→ `hdiutil detach` → `open` 新应用 → 自删除;非 `.dmg` 或异常时回退为 `open` 下载文件。
|
||||
- **Linux**:`xdg-open` 打开下载文件后退出。
|
||||
|
||||
触发入口:macOS 菜单 `Check for Updates...`(手动,显示结果)、绑定方法 `CheckForUpdates()`(手动)与 `AutoCheckForUpdates()`(静默 + 自动下载,前端 `frontend/src/App.vue` 在检测到 `window.go.main.App.AutoCheckForUpdates` 存在时会调用)。
|
||||
|
||||
## 7. Wails 构建配置(cmd/desktop/wails.json)
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "WeKnora Lite",
|
||||
"outputfilename": "WeKnora Lite",
|
||||
"frontend:dir": "../../frontend",
|
||||
"wailsjsdir": "../../frontend/src",
|
||||
"info": { "companyName": "Tencent", "productName": "WeKnora Lite", "productVersion": "1.0.0" },
|
||||
"mac": { "category": "public.app-category.productivity", "titlebar": "hiddenInset" }
|
||||
}
|
||||
```
|
||||
|
||||
要点:
|
||||
|
||||
- `frontend:dir` 指向仓库的 `frontend/`;`wailsjsdir` 指向 `frontend/src`,因此 Wails 自动生成的绑定输出在 `frontend/src/wailsjs/`(`go/main/App.js`、`App.d.ts` 及 `runtime/`)。
|
||||
- **未配置 `frontend:build` 命令**:前端构建不由 Wails 驱动,而是由打包脚本单独执行(见下节);WebView 内容也不是 Wails 静态资源,而是反向代理到内嵌后端。
|
||||
- `cmd/desktop/build/` 仅包含 `appicon.png`(应用图标)与 `darwin/Info.plist`(macOS bundle 的 Go template,声明 `CFBundleIdentifier: com.wails.WeKnora Lite`、最低系统版本 10.13、Retina 支持等);`wails build` 的产物输出到 `cmd/desktop/build/bin/`。
|
||||
|
||||
## 8. 前端如何感知桌面环境
|
||||
|
||||
- `dragHandlerJS` 会给 `document.documentElement` 加上 `wails-desktop` class,前端 CSS 可据此做桌面端样式适配。
|
||||
- Wails 注入的 `window.go.main.App.*`(生成绑定见 `frontend/src/wailsjs/go/main/`)与 `window.runtime`(`frontend/src/wailsjs/runtime/`,如 `BrowserOpenURL`、`EventsEmit`)只在桌面环境存在,前端通过特性检测判断:例如 `frontend/src/composables/useApiBaseUrlDisplay.ts` 轮询读取 `window.__WEKNORA_API_BASE__` 或调用 `window.go.main.App.GetAPIBaseURL()` 来获取真实 API 地址(浏览器环境则回退到配置值 / `window.location.origin`);`frontend/src/App.vue` 检测到 `window.go.main.App.AutoCheckForUpdates` 存在时触发静默更新检查。
|
||||
- 设置页 `frontend/src/views/settings/GeneralSettings.vue` 与 `frontend/src/views/integrations/ApiIntegrationSettings.vue` 亦使用这些绑定展示/修改端口与 LAN 监听等桌面专属选项。
|
||||
|
||||
## 9. 构建方式
|
||||
|
||||
macOS 打包脚本为 `scripts/package-mac-app.sh`(根目录 `Makefile` 中没有 desktop 相关 target):
|
||||
|
||||
```bash
|
||||
# 完整构建(前端 + Wails 打包 + 组装 .app)
|
||||
./scripts/package-mac-app.sh
|
||||
|
||||
# 跳过前端构建(复用已有 web/ 目录)
|
||||
SKIP_FRONTEND=1 ./scripts/package-mac-app.sh
|
||||
```
|
||||
|
||||
脚本流程:
|
||||
|
||||
1. **前端构建**:`cd frontend && npm ci && npm run build`,然后将 `frontend/dist` 同步为仓库根的 `web/`(Lite 后端从 `./web` 提供 SPA)。
|
||||
2. **Wails 构建**:需先安装 Wails CLI(`go install github.com/wailsapp/wails/v2/cmd/wails@latest`),设置 `EDITION=lite`、`GOLANG_PROTOBUF_REGISTRATION_CONFLICT=warn`(规避 Milvus 与 Qdrant gRPC 生成代码的 `common.proto` 描述符注册冲突)等环境变量,从 `scripts/get_version.sh` 取版本号注入 ldflags,然后执行真实构建命令:
|
||||
|
||||
```bash
|
||||
cd cmd/desktop && wails build -clean -tags "sqlite_fts5" -ldflags="$LDFLAGS" -o "WeKnora Lite"
|
||||
```
|
||||
|
||||
该命令的"生成绑定"阶段使用 `-tags bindings` 单独编译 `main_bindings.go`(不连接数据库),并刷新 `frontend/src/wailsjs/` 下的绑定文件。
|
||||
3. **组装产物**:将 `cmd/desktop/build/bin/WeKnora Lite.app` 复制到 `dist/`,并向 `.app/Contents/Resources/` 内塞入 `.env`(来自 `.env.lite.example`)、`config/`、`migrations/sqlite/` 与 `web/` 前端资源。
|
||||
|
||||
最终产物为 `dist/WeKnora Lite.app`,双击即可运行。Windows/Linux 亦可在 `cmd/desktop` 下用 `wails build` 自行构建(更新机制已按 `.exe` / `xdg-open` 做了平台适配),但仓库当前仅提供 macOS 打包脚本与 `build/darwin` 资源。
|
||||
@@ -0,0 +1,44 @@
|
||||
# Chrome 插件(知识管理助手)
|
||||
|
||||
浏览器插件解决的是「看到有用的东西,但懒得复制粘贴进知识库」这件事。装上之后可以在任意网页的侧边栏直接向知识库提问,也可以把当前页面剪藏进去。
|
||||
|
||||
插件本身在 [Chrome 应用商店](https://chromewebstore.google.com/detail/jpemjbopikggjlmikmclgbmkhhopjdgd)(名为「知识管理助手」),配合你自建的 WeKnora 服务使用——它不自带后端,所有数据都写进你自己的实例。
|
||||
|
||||
<Screenshot
|
||||
src="/screenshots/chrome-extension.png"
|
||||
caption="Chrome 插件:网页侧边栏问答与内容剪藏"
|
||||
hint="展示插件侧边栏在某个网页上展开的样子(问答面板与知识库选择器),以及剪藏时的区域选择。" />
|
||||
|
||||
## 能做什么
|
||||
|
||||
| 能力 | 说明 |
|
||||
| --- | --- |
|
||||
| 知识库问答 | 侧边栏对话面板,可切换多个知识库,支持快速 / 深度 / 精确三种回答模式,边浏览边问不打断当前工作 |
|
||||
| 网页剪藏 | 保存页面 URL、由 AI 智能提取正文,或手动框选区域,写入指定知识库 |
|
||||
| Markdown 速记 | 内置编辑器随手记想法,一键存进知识库 |
|
||||
| 快捷键 | 提问、开侧边栏等操作都可自定义快捷键 |
|
||||
|
||||
## 怎么配
|
||||
|
||||
WeKnora 界面里有一个引导页:「设置 → 集成 → Chrome 扩展」,会直接显示当前实例的 API 地址与复制按钮。步骤:
|
||||
|
||||
1. **拿 API 凭证**:在「设置 → API 信息」复制 API Key 与 API 地址。建议单独建一个 Key 并按需收窄能力(至少需要检索与入库相关能力,见[租户、用户与认证授权](../03-features/01-tenant-auth.md)的 API Key 章节);
|
||||
2. **桌面版建议先固定端口**:用 WeKnora 桌面版时,在 API 信息里设一个固定端口(例如 37841)。否则每次启动端口变化,插件会连不上;
|
||||
3. **安装插件**:从 Chrome 应用商店安装;
|
||||
4. **在插件里连接**:打开插件设置,选「企业/开发者」模式,填入 API 地址与 API Key。
|
||||
|
||||
配置完成后建议先让它列一次知识库或问一个问题,确认凭证与网络都通。
|
||||
|
||||
## 排查
|
||||
|
||||
| 现象 | 检查点 |
|
||||
| --- | --- |
|
||||
| 插件提示连不上 | API 地址是否可从浏览器所在机器访问(容器内地址、`localhost` 在远程部署下都不行);桌面版是否固定了端口 |
|
||||
| 401 / 403 | API Key 是否被吊销;Key 的能力是否覆盖了检索与入库;如果 Key 限定了知识库范围,目标库是否在名单里 |
|
||||
| 剪藏后知识库里没有 | 到知识库文档列表看解析状态,`processing` 说明在解析中;失败原因见文档详情 |
|
||||
|
||||
## 相关
|
||||
|
||||
- 凭证与能力:[租户、用户与认证授权](../03-features/01-tenant-auth.md)
|
||||
- 剪藏内容的入库流程:[文档入库流程](../02-architecture/03-document-pipeline.md)
|
||||
- 其它集成方式:[Claw Skill](07-claw-skill.md)、[命令行工具](02-cli.md)、[Go SDK](03-go-sdk.md)
|
||||
@@ -0,0 +1,50 @@
|
||||
# Claw Skill
|
||||
|
||||
Claw Skill 是把 WeKnora 挂给 AI Agent 用的一种方式:安装之后,OpenClaw 生态里的 Agent 就能通过 WeKnora 的 REST API 往知识库里写内容、跨库检索。
|
||||
|
||||
Skill 托管在 ClawHub,包名 [`@lyingbug/weknora`](https://clawhub.ai/lyingbug/weknora)(MIT-0)。它是一层薄封装,实际能力就是 WeKnora 的 REST 接口。
|
||||
|
||||
## 能做什么
|
||||
|
||||
| 能力 | 对应接口 |
|
||||
| --- | --- |
|
||||
| 上传文件 | 把 PDF / Word / Excel 等文档送进知识库并自动解析向量化 |
|
||||
| 导入网页 | 按 URL 抓取正文写入知识库,支持轮询解析状态 |
|
||||
| 写入 Markdown | 以 Markdown 创建或编辑知识条目,适合会议记录、结构化笔记 |
|
||||
| 混合检索 | 单库 `hybrid-search` 与跨库 `knowledge-search`,向量 + 关键词召回 |
|
||||
| 浏览知识库 | 列出知识库与条目、查看详情 |
|
||||
|
||||
## 怎么配
|
||||
|
||||
WeKnora 界面里有引导页:「设置 → 集成 → Claw Skill」,会带上当前实例的 API 地址与可复制的环境变量示例、安装命令。步骤:
|
||||
|
||||
1. **拿 API 凭证**:「设置 → API 信息」里复制 API Key 与 API 地址;
|
||||
2. **配环境变量**:在终端或 `~/.zshrc` / `~/.bashrc` 里设置
|
||||
|
||||
```bash
|
||||
export WEKNORA_BASE_URL=https://your-weknora.example.com/api/v1
|
||||
export WEKNORA_API_KEY=sk-xxxxx
|
||||
```
|
||||
|
||||
3. **安装 Skill**:在装好 OpenClaw CLI 的环境里执行引导页给出的安装命令,或到 ClawHub 页面按指引安装;
|
||||
4. **验证**:让 Agent 列一次知识库或跑一次检索,确认凭证与网络可达。
|
||||
|
||||
## 和 MCP 的关系
|
||||
|
||||
两者都是「把 WeKnora 给外部 Agent 用」,选哪个取决于对方生态:
|
||||
|
||||
| | Claw Skill | MCP Server |
|
||||
| --- | --- | --- |
|
||||
| 面向 | OpenClaw / ClawHub 生态的 Agent | 支持 MCP 协议的客户端(Claude Desktop、VS Code Copilot 等) |
|
||||
| 安装 | ClawHub 安装 Skill | `pip install tencent-weknora-mcp` 或 `uvx` 运行 |
|
||||
| 传输 | 直接调 REST | stdio / SSE / Streamable HTTP |
|
||||
| 能力范围 | 导入、检索、浏览(5 类) | 29 个工具,另含租户、模型、会话、Agent 问答、Wiki |
|
||||
| 文档 | 本篇 | [MCP 集成](../03-features/08-mcp.md) |
|
||||
|
||||
需要更完整的能力(跑 Agent 对话、管模型、读 Wiki)时用 MCP Server;只是想让 Agent 存取资料,Skill 更轻。
|
||||
|
||||
## 相关
|
||||
|
||||
- 凭证与能力收窄:[租户、用户与认证授权](../03-features/01-tenant-auth.md)
|
||||
- 底层接口:[API 总览](../04-api/01-api-overview.md)
|
||||
- 其它集成方式:[Chrome 插件](06-chrome-extension.md)、[MCP 集成](../03-features/08-mcp.md)
|
||||
Reference in New Issue
Block a user