From ec1ab31b0eb3d68ca0e4de50919ebadeec87fbc7 Mon Sep 17 00:00:00 2001 From: kuaifan Date: Wed, 10 Jun 2026 06:59:36 +0000 Subject: [PATCH] =?UTF-8?q?refactor(ai-kb):=20=E7=B4=A2=E5=BC=95=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E6=94=B9=E4=B8=BA=E5=AE=B9=E5=99=A8=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=20reconcile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除 CI reindex workflow(ai-kb-reindex.yml):内容入库不再依赖 CI 远程触发,AI 插件容器每次启动按文件 hash 对账(reconcile)自动增量 收敛,免重启即时生效时可手动调 POST /kb/reindex(默认 mode=reconcile) - ai-kb README.md 同步更新贡献流程与内容同步机制说明 - CLAUDE.md 精简 ai-kb 同步规则:去除与 _schema/README 重复的写作规范 与背景说明,保留同步时机、操作指引、改完无需触发索引三条 - 另:auth 接口 locale 检索语种缺省改为由请求语言推导(含 zh 视为 zh, 否则 en),删除 config/ai.php 的 rag_supported_locales 与回退逻辑, 前端改用 getLanguage() 统一映射,同步更新 ai-kb auth.md - appstore 镜像升级 0.4.3 -> 0.5.0 Co-Authored-By: Claude Opus 4.8 (1M context) Co-Authored-By: Claude Fable 5 --- .github/workflows/ai-kb-reindex.yml | 97 ------------------- CLAUDE.md | 28 +----- .../Controllers/Api/AssistantController.php | 9 +- config/ai.php | 11 +-- docker-compose.yml | 2 +- resources/ai-kb/README.md | 10 +- .../ai-kb/zh/concept/ai-assistant/auth.md | 2 +- .../js/components/AIAssistant/index.vue | 5 +- 8 files changed, 17 insertions(+), 147 deletions(-) delete mode 100644 .github/workflows/ai-kb-reindex.yml diff --git a/.github/workflows/ai-kb-reindex.yml b/.github/workflows/ai-kb-reindex.yml deleted file mode 100644 index 909280a98..000000000 --- a/.github/workflows/ai-kb-reindex.yml +++ /dev/null @@ -1,97 +0,0 @@ -name: "AI-KB Reindex" - -# 主仓库 ai-kb 内容变更后,远程触发 AI 插件 `POST /kb/reindex` 增量入库。 -# 失败不阻塞 PR/发布;AI 容器下次启动会自动 ingest_all 兜底(plan §三 / R9)。 - -on: - push: - branches: - - "pro" - paths: - - "resources/ai-kb/**" - workflow_dispatch: - inputs: - mode: - description: "incremental | full" - required: false - default: "incremental" - paths: - description: "JSON 数组,相对 resources/ai-kb/ 的路径(mode=incremental 时使用)" - required: false - default: "[]" - -jobs: - reindex: - permissions: - contents: read - runs-on: ubuntu-latest - timeout-minutes: 5 - # 不被其他 job 依赖;失败不阻塞合并。 - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 2 # 用于 git diff HEAD~1 HEAD - - - name: Resolve changed paths - id: paths - run: | - set -euo pipefail - MODE="${{ github.event.inputs.mode || 'incremental' }}" - - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - INPUT_PATHS='${{ github.event.inputs.paths }}' - if [ -z "${INPUT_PATHS}" ] || [ "${INPUT_PATHS}" = "[]" ]; then - MODE="full" - PATHS_JSON="[]" - else - PATHS_JSON="${INPUT_PATHS}" - fi - else - CHANGED=$(git diff --name-only HEAD~1 HEAD -- 'resources/ai-kb/**/*.md' 'resources/ai-kb/**/*.yaml' 'resources/ai-kb/**/*.yml' || true) - if [ -z "${CHANGED}" ]; then - echo "No ai-kb file changes detected; skip reindex call." - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - PATHS_JSON=$(printf '%s\n' "${CHANGED}" \ - | sed 's|^resources/ai-kb/||' \ - | jq -R . | jq -cs .) - fi - - echo "mode=${MODE}" >> "$GITHUB_OUTPUT" - echo "paths_json=${PATHS_JSON}" >> "$GITHUB_OUTPUT" - echo "skip=false" >> "$GITHUB_OUTPUT" - - - name: Trigger /kb/reindex - if: steps.paths.outputs.skip != 'true' - env: - AI_SERVICE_URL: ${{ secrets.AI_SERVICE_URL }} - KB_INGEST_TOKEN: ${{ secrets.KB_INGEST_TOKEN }} - MODE: ${{ steps.paths.outputs.mode }} - PATHS_JSON: ${{ steps.paths.outputs.paths_json }} - run: | - set -euo pipefail - if [ -z "${AI_SERVICE_URL:-}" ] || [ -z "${KB_INGEST_TOKEN:-}" ]; then - echo "::warning ::AI_SERVICE_URL or KB_INGEST_TOKEN secret not set; skip remote reindex." - exit 0 - fi - - BODY=$(jq -cn --arg mode "$MODE" --argjson paths "$PATHS_JSON" '{mode:$mode, paths:$paths}') - echo "Posting body: $BODY" - - HTTP_CODE=$(curl -sS -o /tmp/kb-reindex.json -w '%{http_code}' \ - --max-time 90 \ - --retry 2 --retry-delay 5 --retry-connrefused \ - -X POST "${AI_SERVICE_URL%/}/kb/reindex" \ - -H "X-Ingest-Token: ${KB_INGEST_TOKEN}" \ - -H 'Content-Type: application/json' \ - -d "$BODY" || true) - - echo "HTTP $HTTP_CODE" - cat /tmp/kb-reindex.json || true - - if [ "$HTTP_CODE" != "200" ]; then - echo "::warning ::reindex returned HTTP $HTTP_CODE — AI 容器下次启动会 ingest_all 兜底(参见 plan §三/R9)" - exit 0 # 不阻塞 push / 发布 - fi diff --git a/CLAUDE.md b/CLAUDE.md index 8c32212b6..c7c1fbe87 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,31 +49,13 @@ Laravel 8 (LaravelS/Swoole) + Vue 2 (Vite) + Electron。开源任务/项目管 - 新增用户可见文本须追加原文(简体中文)到:前端 `language/original-web.txt`,后端 `language/original-api.txt`(去重) - 前端翻译用 `$L("文本")`,动态值用 `(*)` 占位:`$L('共(*)条', n)`——禁止拼接翻译 -## DooTask AI 知识库 (ai-kb) 同步规则 +## ai-kb 同步规则 -`dootask/resources/ai-kb/` 是**专给 AI 助手 RAG 检索的功能知识库**,与人类文档(`dootask-website`)独立。它直接影响产品内 AI 助手能否正确回答用户的「X 怎么用」。 +`resources/ai-kb/` 是产品内 AI 助手 RAG 检索的功能知识库(目录结构、写作规范、索引机制见其 `README.md` 与 `_schema/`)。 -**何时必须同步更新 ai-kb**: - -- 新增、修改、删除任何用户可见的功能、菜单、按钮、流程、字段 -- 调整 API 行为(错误码、参数含义、返回结构) -- 引入新插件 / 微应用,或修改权限 / 角色定义 - -**操作步骤**: - -1. 在 `resources/ai-kb/_meta/feature-map.yaml` 找到对应 `feature` 的 chunk 清单 -2. 按 `resources/ai-kb/_schema/chunk-style.md` 风格修改对应 markdown -3. 更新 frontmatter 的 `last_verified` 字段为当前主程序版本号 -4. 该功能没有对应 chunk 时,按 `resources/ai-kb/_schema/frontmatter.md` 规范新建 - -**禁止**: - -- 跨章节指代("如上图所示"、"在前面一节")——RAG 切块后会丢失上下文 -- 把 dootask-website 的人类教程或截图直接复制过来 -- 单独提交「只改 ai-kb」的 PR——应与触发它的主代码改动同一个 PR -- 改产品代码但不改 ai-kb(PR review 应拦截) - -**自动化**:合入 main 后 CI 自动调用 AI 插件的 `POST /kb/reindex` 增量入库;失败由 AI 容器重启自动 `ingest_all` 兜底。 +- **同步时机**:改动用户可见的功能/菜单/按钮/流程/字段、API 行为(错误码、参数含义、返回结构)、插件/微应用、权限/角色定义时,必须在同一次提交中同步更新 ai-kb,不要把 ai-kb 改动单独拆成一个提交 +- **怎么改**:在 `_meta/feature-map.yaml` 找到对应 feature 的 chunk 清单,按 `_schema/chunk-style.md` 与 `_schema/frontmatter.md` 修改或新建 chunk,并把 frontmatter 的 `last_verified` 更新为当前主程序版本号 +- **改完即止**:无需触发任何索引操作,插件容器启动时会自动对账收敛 ## Playwright 测试 diff --git a/app/Http/Controllers/Api/AssistantController.php b/app/Http/Controllers/Api/AssistantController.php index 5384e7f8b..2b49c5d5c 100644 --- a/app/Http/Controllers/Api/AssistantController.php +++ b/app/Http/Controllers/Api/AssistantController.php @@ -32,6 +32,7 @@ class AssistantController extends AbstractController * @apiParam {String} model_type 模型类型 * @apiParam {String} model_name 模型名称 * @apiParam {JSON} context 上下文数组 + * @apiParam {String} [locale] ai-kb 检索语种:zh、en(缺省取请求语言 language,包含 zh 视为 zh,否则 en) * * @apiSuccess {Number} ret 返回状态码(1正确、0错误) * @apiSuccess {String} msg 返回信息(错误描述) @@ -46,12 +47,8 @@ class AssistantController extends AbstractController $modelType = trim(Request::input('model_type', '')); $modelName = trim(Request::input('model_name', '')); $contextInput = Request::input('context', []); - // ai-kb 检索语种;缺省 zh,前端传 'zh' / 'en' - $supportedLocales = config('ai.rag_supported_locales', ['zh', 'en']); - $locale = trim(Request::input('locale', 'zh')); - if (!in_array($locale, $supportedLocales, true)) { - $locale = 'zh'; - } + $locale = trim(Request::input('locale', '')) ?: trim(Base::headerOrInput('language')); + $locale = str_contains(strtolower($locale), 'zh') ? 'zh' : 'en'; // 灰度判定(参考 config/ai.php):总开关 + canary 白名单 $ragEnabled = AI::ragEnabledFor((int) $user->userid); diff --git a/config/ai.php b/config/ai.php index 27ec8fdd0..3924a40bd 100644 --- a/config/ai.php +++ b/config/ai.php @@ -5,7 +5,7 @@ | DooTask AI 助手灰度配置 |-------------------------------------------------------------------------- | -| RAG(帮助知识库检索)功能上线时按以下顺序灰度(参考 plan §八): +| RAG(帮助知识库检索)功能上线时按以下顺序灰度: | Stage 1 — staging:RAG_ENABLED=true 仅 staging 环境,全体可用 | Stage 2 — canary:RAG_ENABLED=true + RAG_CANARY_USERIDS="1,2,3,4,5" | 仅白名单 user 命中 RAG @@ -45,13 +45,4 @@ return [ | 有值表示 仅白名单 userid 命中 RAG(Stage 2 canary)。 */ 'rag_canary_userids' => env('RAG_CANARY_USERIDS', ''), - - /* - |-------------------------------------------------------------------------- - | RAG 检索语种受控集合 - |-------------------------------------------------------------------------- - | 前端可传的 locale 值;不在集合内默认回退到 zh。 - | P0 仅启用 zh;P1 起开放 en。 - */ - 'rag_supported_locales' => ['zh', 'en'], ]; diff --git a/docker-compose.yml b/docker-compose.yml index 6ab04c3fe..85ca86280 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -98,7 +98,7 @@ services: appstore: container_name: "dootask-appstore-${APP_ID}" privileged: true - image: "dootask/appstore:0.4.3" + image: "dootask/appstore:0.5.1" volumes: - shared_data:/usr/share/dootask - ${HOST_DOCKER_SOCK:-/var/run/docker.sock}:/var/run/docker.sock diff --git a/resources/ai-kb/README.md b/resources/ai-kb/README.md index 2f540e389..b7e57d667 100644 --- a/resources/ai-kb/README.md +++ b/resources/ai-kb/README.md @@ -11,7 +11,7 @@ ai-kb/ ├── _schema/ 写作规范(必读) │ ├── frontmatter.md frontmatter 字段规范 + 受控词表 │ └── chunk-style.md chunk 写作风格 + 正反例 -├── _meta/ 元数据(CI 与脚本读取) +├── _meta/ 元数据(lint / eval 脚本读取) │ ├── feature-map.yaml feature 全集 + 每个 feature 的 chunk 清单 │ └── tool-binding.yaml chunk ↔ MCP 工具映射 ├── _eval/ 回归测试 @@ -46,7 +46,7 @@ ai-kb/ 2. 通读 [`_schema/chunk-style.md`](./_schema/chunk-style.md) — 写作风格与正反例 3. 在 [`_meta/feature-map.yaml`](./_meta/feature-map.yaml) 找到对应 feature 的 chunk 清单和归属批次 4. 在对应 `zh///.md` 路径下新建文件 -5. 提交 PR,CI 会自动跑 lint;通过且 review 完毕后合入 main,CI 自动触发 AI 插件的 `POST /kb/reindex` 入库 +5. 提交 PR、review 后合入。内容进索引不需要额外操作:AI 插件容器每次启动会按文件 hash 对账(reconcile),自动增量收敛新增/变更/删除;想免重启即时生效可手动调 `POST /kb/reindex` ## 改 DooTask 主程序后必须同步更新这里 @@ -64,15 +64,13 @@ volumes: - ../../../dootask/resources/ai-kb:/app/kb-content:ro ``` -触发入库(CI 或运维手动): +内容同步机制:容器每次启动按文件 hash 对账(reconcile),自动增量收敛新增/变更/删除的 markdown——客户实例更新 DooTask 后重启插件容器即生效。需要免重启即时生效时手动触发: ```bash curl -X POST 'http://ai-service/kb/reindex' \ -H "X-Ingest-Token: $KB_INGEST_TOKEN" \ - -d '{"paths":["zh/howto/task-create.md"], "mode":"incremental"}' + -d '{"mode":"reconcile"}' ``` -容器启动时 lifespan 会自动跑一次 `ingest_all` 作为兜底。 - ## 维护责任 - **内容**:产品功能负责人 / PM / 技术写作者按 `_meta/feature-map.yaml` 中的 `owner` 列认领 diff --git a/resources/ai-kb/zh/concept/ai-assistant/auth.md b/resources/ai-kb/zh/concept/ai-assistant/auth.md index 0798bb626..778bb9d28 100644 --- a/resources/ai-kb/zh/concept/ai-assistant/auth.md +++ b/resources/ai-kb/zh/concept/ai-assistant/auth.md @@ -30,7 +30,7 @@ AI 助手发送提问前必须先调用 `POST api/assistant/auth` 生成一次 ## 数据流 1. 用户点发送 -2. 前端 `POST api/assistant/auth`,参数:`model_type`、`model_name`、`context`(JSON)、`locale`(zh/en) +2. 前端 `POST api/assistant/auth`,参数:`model_type`、`model_name`、`context`(JSON)、`locale`(zh/en,缺省取请求语言,语言包含 zh 视为 zh,否则 en) 3. 后端 `AssistantController::auth` 校验登录 + 聊天权限,写入临时凭证 4. 返回 `{stream_key: "xxx"}` 5. 前端开 SSE:`ai/invoke/stream/{stream_key}` diff --git a/resources/assets/js/components/AIAssistant/index.vue b/resources/assets/js/components/AIAssistant/index.vue index 93095e710..19d4e55a8 100644 --- a/resources/assets/js/components/AIAssistant/index.vue +++ b/resources/assets/js/components/AIAssistant/index.vue @@ -241,6 +241,7 @@ import FloatButton from "./float-button.vue"; import AssistantModal from "./modal.vue"; import PromptImage from "./prompt-image.vue"; import {buildWeakPrompt, renderWeakPromptText} from "./page-context"; +import {getLanguage} from "../../language"; import {getWelcomePrompts} from "./welcome-prompts"; export default { @@ -1004,9 +1005,7 @@ export default { * 请求 stream_key */ async fetchStreamKey({model_type, model_name, context}) { - // ai-kb 检索语种;'zh' / 'en'。基于 localStorage 当前语言简单映射 - const lang = (window.localStorage.getItem('__system:languageName__') || 'zh').toLowerCase(); - const locale = lang.startsWith('en') ? 'en' : 'zh'; + const locale = /zh/i.test(getLanguage()) ? 'zh' : 'en'; const payload = { model_type, model_name,