From 524606463ba20f936a477f05244536a01760be42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E8=89=B2?= <38353004+fsqinghuayu@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:18:58 +0800 Subject: [PATCH 01/20] Update docker-image.yml --- .github/workflows/docker-image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 4c00ddc..aba316e 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -49,7 +49,7 @@ jobs: with: context: . file: Dockerfile - platforms: linux/amd64 + platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} From 2f00b0d2610bd64cae33c56dd82a6b9e7f5528f8 Mon Sep 17 00:00:00 2001 From: huangjunjia <2489441209@qq.com> Date: Tue, 24 Mar 2026 10:50:09 +0800 Subject: [PATCH 02/20] feat: add oc result event SSE --- app/api/routes_chat.py | 54 +++++++++++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/app/api/routes_chat.py b/app/api/routes_chat.py index 9398f51..fe1fbe9 100644 --- a/app/api/routes_chat.py +++ b/app/api/routes_chat.py @@ -846,6 +846,9 @@ async def _run_openclaw_cmd(): prefix = "\n\n".join(prefix_parts) + "\n\n" final_user_prompt = prefix + user_prompt + collected_text_chunks: list[str] = [] + file_regex = re.compile(r"^- `?(\/[\S`]+)`?", re.MULTILINE) + async for event in oc_chat_completions_sse( oc_session_key=oc_session_key, user_prompt=final_user_prompt, @@ -853,32 +856,51 @@ async def _run_openclaw_cmd(): ): # event 是 bytes,需要对应处理 if event.strip() == b"data: [DONE]": - - # check_cmd = """find . -maxdepth 4 \( -path "./claude" -o -path "./claude/*" -o -path "./node_modules" -o -path "./node_modules/*" -o -path "./.git" -o -path "./.git/*" -o -path "./venv" -o -path "./venv/*" -o -path "./.venv" -o -path "./.venv/*" -o -path "./env" -o -path "./env/*" -o -path "./__pycache__" -o -path "./__pycache__/*" \) -prune -o -type f \( -name "package.json" -o -name "pnpm-lock.yaml" -o -name "yarn.lock" -o -name "package-lock.json" -o -name "next.config.*" -o -name "vite.config.*" -o -name "vue.config.*" -o -name "nuxt.config.*" -o -name "svelte.config.*" -o -name "astro.config.*" -o -name "remix.config.*" -o -name "angular.json" -o -name "gatsby-config.*" -o -path "./index.html" -o -path "*/public/index.html" -o -path "./server.js" -o -path "./app.js" -o -path "./index.js" -o -path "./main.js" -o -path "./server.ts" -o -path "./app.ts" -o -path "./index.ts" -o -path "./main.ts" -o -path "./src/index.js" -o -path "./src/index.ts" -o -path "./src/index.jsx" -o -path "./src/index.tsx" -o -path "./src/main.js" -o -path "./src/main.ts" -o -path "./src/main.jsx" -o -path "./src/main.tsx" -o -path "./src/App.vue" -o -path "./src/app.js" -o -path "./src/app.ts" -o -path "./src/app.jsx" -o -path "./src/app.tsx" -o -path "./src/server.js" -o -path "./src/server.ts" -o \( -path "./src/*" -a \( -name "*.js" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" -o -name "*.vue" \) \) \)""" + # 在 DONE 时,从之前累计的文本块里提取路径并插入一个额外 chunk + combined = "".join(collected_text_chunks) + matches = file_regex.findall(combined) + if matches: + deploy_check_info = json.dumps({ + "type": "result", + "is_error": False, + "result": "\n".join(f"-`{m}`"for m in matches), + }, ensure_ascii=False) + chunk = gpt_stream_chunk(deploy_check_info) + yield f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n".encode("utf-8") if payload.enable_pre_deploy_check: - pre_deploy_check_result = await runner.exec_json(command=check_cmd, cwd=workspace_path) if pre_deploy_check_result.exit_code == 0: - if pre_deploy_check_result.stdout: - deploy_check_info = json.dumps({ - "type": "pre_deploy_check", - "success": True, - "find_file": pre_deploy_check_result.stdout, - }) - else: - deploy_check_info = json.dumps({ - "type": "pre_deploy_check", - "success": False, - "find_file": pre_deploy_check_result.stdout, - }) + deploy_check_info = json.dumps({ + "type": "pre_deploy_check", + "success": bool(pre_deploy_check_result.stdout), + "find_file": pre_deploy_check_result.stdout, + }, ensure_ascii=False) # 插入自定义文本 chunk - chunk = gpt_stream_chunk(f"{json.dumps(deploy_check_info, ensure_ascii=False)}") + chunk = gpt_stream_chunk(deploy_check_info) yield f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n".encode("utf-8") + # 放行 [DONE] yield event else: + # 非 DONE:透传,同时尽量从 OpenAI chunk 中抽取文本累计 + try: + line = event.decode("utf-8", errors="ignore").strip() + if line.startswith("data: "): + payload_json = line[6:] + if payload_json and payload_json != "[DONE]": + obj = json.loads(payload_json) + content = ( + obj.get("choices", [{}])[0] + .get("delta", {}) + .get("content") + ) + if isinstance(content, str) and content: + collected_text_chunks.append(content) + except Exception: + pass + yield event # 处理messages From 5d94f9ea4b62a30032484de9ad0e88e53c08087b Mon Sep 17 00:00:00 2001 From: huangjunjia <2489441209@qq.com> Date: Tue, 24 Mar 2026 10:56:41 +0800 Subject: [PATCH 03/20] fix(docker): update file permission --- Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 93f6786..ab08f2d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,7 +48,7 @@ RUN openclaw plugins install @openclaw-china/channels # 把插件数据备份到不会被挂载覆盖的目录 RUN mkdir -p /app/.openclaw-extensions-backup && \ - cp -a /home/user/.openclaw/extensions /app/.openclaw-extensions-backup/ + cp -r /home/user/.openclaw/extensions /app/.openclaw-extensions-backup/ EXPOSE 8000 18789 @@ -56,7 +56,7 @@ EXPOSE 8000 18789 CMD ["sh", "-c", "\ if [ ! -d \"/home/user/.openclaw/extensions/channels\" ]; then \ mkdir -p /home/user/.openclaw/extensions && \ - cp -a /app/.openclaw-extensions-backup/extensions/* /home/user/.openclaw/extensions/ 2>/dev/null || true; \ + cp -r /app/.openclaw-extensions-backup/extensions/* /home/user/.openclaw/extensions/ 2>/dev/null || true; \ echo 'Restored openclaw extensions (channels plugin was missing)'; \ else \ echo 'channels plugin exists, skipping restore'; \ @@ -64,7 +64,7 @@ CMD ["sh", "-c", "\ mkdir -p /home/user/.claude/skills && \ for p in /app/.skills-backup/*; do \ name=\"$(basename \"$p\")\"; \ - cp -a \"$p\" /home/user/.claude/skills/ 2>/dev/null || true; \ + cp -r \"$p\" /home/user/.claude/skills/ 2>/dev/null || true; \ echo \"Restored skill entry (overwrite): $name\"; \ done && \ openclaw gateway run --port 18789 --bind lan & \ From b1f61cbcbb6a8c9299d31905facda1ae3b4509ca Mon Sep 17 00:00:00 2001 From: huangjunjia <2489441209@qq.com> Date: Tue, 24 Mar 2026 15:44:39 +0800 Subject: [PATCH 04/20] =?UTF-8?q?feat:=20switch=C2=A0to=C2=A0root=C2=A0and?= =?UTF-8?q?=C2=A0add=20entrypoint=20script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 43 +++++++++++++------------------------------ entrypoint.sh | 23 +++++++++++++++++++++++ 2 files changed, 36 insertions(+), 30 deletions(-) create mode 100644 entrypoint.sh diff --git a/Dockerfile b/Dockerfile index ab08f2d..9634e40 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,14 +20,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && rm -rf /root/.npm \ && rm -rf /tmp/* -# 创建用户 -RUN groupadd -r user && useradd -r -g user -m -s /bin/bash user - # 创建目录并设置权限 -RUN mkdir -p /data /data/user /app /home/user/.claude/skills /home/user/.openclaw /home/user/db && \ - chown -R user:user /data /app /home/user && \ - chmod 755 /data /app /home/user && \ - chmod 775 /home/user/db /home/user/.claude /home/user/.claude/skills /home/user/.openclaw +RUN mkdir -p /data /app /home/user/.claude/skills /home/user/.openclaw /home/user/db && \ + chmod 755 /home/user/.openclaw /home/user/.claude /home/user/.claude/skills && \ + chmod 755 /data /app /home/user # 安装 Python 依赖 WORKDIR /app @@ -35,38 +31,25 @@ COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt # 复制代码 -USER user -COPY --chown=user:user . /app +COPY . /app # 复制自定义 skills 到不会被挂载覆盖的目录 -COPY --chown=user:user skills/ /app/.skills-backup/ +COPY skills/ /app/.skills-backup/ ENV HOME=/home/user # 安装插件 -RUN openclaw plugins install @openclaw-china/channels +RUN openclaw plugins install @openclaw-china/channels@2026.3.10 +RUN #npx -y @tencent-weixin/openclaw-weixin-cli@latest install # 把插件数据备份到不会被挂载覆盖的目录 RUN mkdir -p /app/.openclaw-extensions-backup && \ - cp -r /home/user/.openclaw/extensions /app/.openclaw-extensions-backup/ + cp -a /home/user/.openclaw/extensions /app/.openclaw-extensions-backup/ + +# 复制启动脚本 +COPY entrypoint.sh /app/entrypoint.sh +RUN chmod +x /app/entrypoint.sh EXPOSE 8000 18789 -# 启动时检查 channels 插件是否存在,不存在才恢复 -CMD ["sh", "-c", "\ - if [ ! -d \"/home/user/.openclaw/extensions/channels\" ]; then \ - mkdir -p /home/user/.openclaw/extensions && \ - cp -r /app/.openclaw-extensions-backup/extensions/* /home/user/.openclaw/extensions/ 2>/dev/null || true; \ - echo 'Restored openclaw extensions (channels plugin was missing)'; \ - else \ - echo 'channels plugin exists, skipping restore'; \ - fi && \ - mkdir -p /home/user/.claude/skills && \ - for p in /app/.skills-backup/*; do \ - name=\"$(basename \"$p\")\"; \ - cp -r \"$p\" /home/user/.claude/skills/ 2>/dev/null || true; \ - echo \"Restored skill entry (overwrite): $name\"; \ - done && \ - openclaw gateway run --port 18789 --bind lan & \ - uvicorn main:app --host 0.0.0.0 --port 8000\ - "] +CMD ["/app/entrypoint.sh"] diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..ea5e636 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# 恢复插件(只替换备份中存在的,不影响用户自装的插件) +mkdir -p /home/user/.openclaw/extensions +for p in /app/.openclaw-extensions-backup/extensions/*; do + name="$(basename "$p")" + rm -rf "/home/user/.openclaw/extensions/$name" + cp -a "$p" /home/user/.openclaw/extensions/ + echo "Restored openclaw extension (overwrite): $name" +done +chmod -R 755 /home/user/.openclaw + +# 恢复 skills +mkdir -p /home/user/.claude/skills +for p in /app/.skills-backup/*; do + name="$(basename "$p")" + cp -a "$p" /home/user/.claude/skills/ 2>/dev/null || true + echo "Restored skill entry (overwrite): $name" +done + +# 启动服务 +openclaw gateway run --port 18789 --bind lan & +uvicorn main:app --host 0.0.0.0 --port 8000 From e456ac01c089c18733e6b90d1953840ecc84bc97 Mon Sep 17 00:00:00 2001 From: huangjunjia <2489441209@qq.com> Date: Tue, 24 Mar 2026 15:45:29 +0800 Subject: [PATCH 05/20] fix: update the interface URL --- app/core/request_id_middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/core/request_id_middleware.py b/app/core/request_id_middleware.py index 5773f1d..4c34095 100644 --- a/app/core/request_id_middleware.py +++ b/app/core/request_id_middleware.py @@ -48,7 +48,7 @@ async def dispatch(self, request: Request, call_next): "/302/claude-code/messages", "/302/claude-code/skills/detail", "/302/claude-code/chat/completions", - "/302/claude-code/sandbox/execute/stream", + "/302/claude-code/commands/stream", "/api/v1/chat/completions" ] if request.url.path in stream_url_path: From 143e67382ff5ad02e75b08b325b43cd30d1ebb96 Mon Sep 17 00:00:00 2001 From: huangjunjia <2489441209@qq.com> Date: Tue, 24 Mar 2026 17:51:27 +0800 Subject: [PATCH 06/20] fix: 302ai-search forcibly cannot be deleted --- app/api/routes_skill.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/routes_skill.py b/app/api/routes_skill.py index c75bff6..061d5ac 100644 --- a/app/api/routes_skill.py +++ b/app/api/routes_skill.py @@ -323,7 +323,7 @@ def _put_once(k=key, d=desc, z=zh): "description_zh": zh_by_md5.get(_md5_16(s.get("description"))) if isinstance(s.get("description"), str) and s.get("description") else "", - "source": (s.get("source") or "") if isinstance(s.get("source"), str) else "", + "source":"openclaw-bundled" if (isinstance(s.get("name"), str) and s.get("name") == "302ai-search") else ((s.get("source") or "") if isinstance(s.get("source"), str) else ""), "eligible": bool(s.get("eligible")) if "eligible" in s else None, "disabled": bool(s.get("disabled")) if "disabled" in s else None, "bundled": bool(s.get("bundled")) if "bundled" in s else None, From 605eaaad6b88769846a0d0dcb6fee59e69202bec Mon Sep 17 00:00:00 2001 From: JI4JUN Date: Tue, 24 Mar 2026 19:18:30 +0800 Subject: [PATCH 07/20] chore: bump openclaw version to 2026.3.23 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9634e40..9bf9164 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ && apt-get install -y --no-install-recommends nodejs \ && npm install -g @anthropic-ai/claude-code@latest \ - && npm install -g openclaw@2026.3.13 \ + && npm install -g openclaw@latest \ && npm install -g clawhub@latest \ && npm install -g @playwright/cli@latest \ && apt-get clean \ From 0f10dcd13000c4e879d839aa86e7f5d87e3faee8 Mon Sep 17 00:00:00 2001 From: huangjunjia <2489441209@qq.com> Date: Wed, 25 Mar 2026 15:41:44 +0800 Subject: [PATCH 08/20] fix: update openclaw version & preinstall acpx --- Dockerfile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9634e40..698c00b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ && apt-get install -y --no-install-recommends nodejs \ && npm install -g @anthropic-ai/claude-code@latest \ - && npm install -g openclaw@2026.3.13 \ + && npm install -g openclaw@latest \ && npm install -g clawhub@latest \ && npm install -g @playwright/cli@latest \ && apt-get clean \ @@ -39,7 +39,8 @@ COPY skills/ /app/.skills-backup/ ENV HOME=/home/user # 安装插件 -RUN openclaw plugins install @openclaw-china/channels@2026.3.10 +RUN openclaw plugins install @openclaw-china/channels@latest +RUN openclaw plugins install @openclaw/acpx RUN #npx -y @tencent-weixin/openclaw-weixin-cli@latest install # 把插件数据备份到不会被挂载覆盖的目录 From ac82dba6b9fd8b626506704669fbfe37de83a51c Mon Sep 17 00:00:00 2001 From: huangjunjia <2489441209@qq.com> Date: Wed, 25 Mar 2026 15:42:34 +0800 Subject: [PATCH 09/20] fix: oc 3.22-2version skill list format --- app/api/routes_skill.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/api/routes_skill.py b/app/api/routes_skill.py index 061d5ac..c276683 100644 --- a/app/api/routes_skill.py +++ b/app/api/routes_skill.py @@ -243,11 +243,11 @@ async def skill_list( oc_result = await oc_runner.exec_json("openclaw skills list --json") oc_skills: list[dict] = [] - if oc_result.exit_code == 0 and oc_result.stdout: + if oc_result.exit_code == 0: try: import json - loaded = json.loads(oc_result.stdout) + loaded = json.loads(oc_result.stdout or oc_result.stderr) # 升级到openclaw 2026.3.22-2版本后,获取skills的结果意外输出到stderr # openclaw skills list --json 输出为 { ..., "skills": [...] } if isinstance(loaded, dict) and isinstance(loaded.get("skills"), list): oc_skills = [x for x in loaded["skills"] if isinstance(x, dict)] From d49c9e28a0d31dd0e7688ae8f6b54258818298a2 Mon Sep 17 00:00:00 2001 From: huangjunjia <2489441209@qq.com> Date: Wed, 25 Mar 2026 17:01:29 +0800 Subject: [PATCH 10/20] feat: add skill favorite functionality --- app/api/routes_skill.py | 75 ++++++++++++++++++++++++++++-- app/models/skill_favorite.py | 19 ++++++++ app/repositories/skill_favorite.py | 63 +++++++++++++++++++++++++ 3 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 app/models/skill_favorite.py create mode 100644 app/repositories/skill_favorite.py diff --git a/app/api/routes_skill.py b/app/api/routes_skill.py index c276683..5890ff4 100644 --- a/app/api/routes_skill.py +++ b/app/api/routes_skill.py @@ -34,6 +34,8 @@ from pydantic import BaseModel, Field +from app.repositories.skill_favorite import SkillFavoriteRepository + router = APIRouter() @@ -42,10 +44,18 @@ class SkillDeleteRequest(BaseModel): skill_list: list = Field([], description="skill_name list") skill_id_list: list = Field([], description="skill_id list") +class SkillFavoriteAddRequest(BaseModel): + skill_list: list = Field([], description="skill_name list") + +class SkillFavoriteCancelRequest(BaseModel): + skill_list: list = Field([], description="skill_name list") + def get_skill_desc_cache_repo(db=Depends(get_db)) -> SkillDescZhCacheRepository: return SkillDescZhCacheRepository(db) +def get_skill_favorite_repo(db=Depends(get_db)) -> SkillFavoriteRepository: + return SkillFavoriteRepository(db) @router.post("/skills") async def create_skill(request: Request, repo: SkillDescZhCacheRepository = Depends(get_skill_desc_cache_repo)): @@ -237,6 +247,7 @@ async def skill_list( limit: int = Query(50, description="每页数量"), offset: int = Query(0, description="偏移量"), cache_repo: SkillDescZhCacheRepository = Depends(get_skill_desc_cache_repo), + favorite_skill_repo: SkillFavoriteRepository = Depends(get_skill_favorite_repo) ): # 直接通过 openclaw CLI 获取 skill 列表(包含 source 等信息) oc_runner = CommandRunner() @@ -313,6 +324,35 @@ def _put_once(k=key, d=desc, z=zh): await run_in_threadpool(_put_once) zh_by_md5[key] = zh + # 1. 假设这是你从 repo 获取到的收藏列表(按时间倒序:['skillA', 'skillB']) + # 请确保在实际代码中调用 repo 获取这个列表 + favorite_skills = await run_in_threadpool( + lambda: favorite_skill_repo.list() + ) + + # --- 排序逻辑开始 --- + + # 构建一个优先级字典:{ "skill_name": 排序权重 } + # 收藏的技能按照列表顺序获得权重(0, 1, 2...),未收藏的统一给一个很大的权重(例如 99999) + priority_map = {name: idx for idx, name in enumerate(favorite_skills)} + + def get_sort_key(item): + name = item.get("name", "") + if name in priority_map: + # 第一梯队:已收藏。保留收藏列表的顺序(index 越小越靠前) + return (0, priority_map[name]) + else: + # 第二梯队:未收藏。这里可以选择按名字字母排序,方便查阅 + return (1, name) + + # 先过滤掉非字典的数据,然后根据规则排序 + sorted_items = sorted( + [s for s in items if isinstance(s, dict)], + key=get_sort_key + ) + + # --- 排序逻辑结束 --- + return ok( { "user_skills": [ @@ -323,15 +363,17 @@ def _put_once(k=key, d=desc, z=zh): "description_zh": zh_by_md5.get(_md5_16(s.get("description"))) if isinstance(s.get("description"), str) and s.get("description") else "", - "source":"openclaw-bundled" if (isinstance(s.get("name"), str) and s.get("name") == "302ai-search") else ((s.get("source") or "") if isinstance(s.get("source"), str) else ""), + "source": "openclaw-bundled" if ( + isinstance(s.get("name"), str) and s.get("name") == "302ai-search") else ( + (s.get("source") or "") if isinstance(s.get("source"), str) else ""), "eligible": bool(s.get("eligible")) if "eligible" in s else None, "disabled": bool(s.get("disabled")) if "disabled" in s else None, "bundled": bool(s.get("bundled")) if "bundled" in s else None, "blockedByAllowlist": bool(s.get("blockedByAllowlist")) if "blockedByAllowlist" in s else None, "missing": s.get("missing") if isinstance(s.get("missing"), dict) else None, } - for s in items - if isinstance(s, dict) + # 这里改用排序后的 sorted_items + for s in sorted_items ], "builtin_skills": [], "project_skills": [], @@ -396,3 +438,30 @@ async def skill_delete( return ok({"data": {"result": delete_result}}) + +@router.post("/skills/favorite/add") +async def skill_favorite_add(payload: SkillFavoriteAddRequest, + repo: SkillFavoriteRepository = Depends(get_skill_favorite_repo)): + + def op(): + with repo.atomic(): + for skill_name in payload.skill_list: + repo.add(skill_name=skill_name) + + await run_in_threadpool(op) + + return ok() + + +@router.post("/skills/favorite/cancel") +async def skill_favorite_add(payload: SkillFavoriteCancelRequest, + repo: SkillFavoriteRepository = Depends(get_skill_favorite_repo)): + + def op(): + with repo.atomic(): + for skill_name in payload.skill_list: + repo.delete(skill_name=skill_name) + + await run_in_threadpool(op) + + return ok() \ No newline at end of file diff --git a/app/models/skill_favorite.py b/app/models/skill_favorite.py new file mode 100644 index 0000000..8d2e4eb --- /dev/null +++ b/app/models/skill_favorite.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +import datetime +import hashlib + +from peewee import AutoField, CharField, DateTimeField, TextField, BooleanField + +from app.models.base import BaseModel + +class SkillFavorite(BaseModel): + id = AutoField() + skill_name = CharField(max_length=64, unique=True) + favorite_time = DateTimeField(default=datetime.datetime.now) + + class Meta: + table_name = "skill_favorites" + indexes = ( + (('favorite_time',), False), + ) diff --git a/app/repositories/skill_favorite.py b/app/repositories/skill_favorite.py new file mode 100644 index 0000000..647ea8a --- /dev/null +++ b/app/repositories/skill_favorite.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from contextlib import contextmanager +from typing import Iterator + +from peewee import SqliteDatabase + +from app.models.base import bind_models +from app.models.skill_favorite import SkillFavorite + + +class SkillFavoriteRepository: + def __init__(self, db: SqliteDatabase): + self.db = db + # 绑定模型到数据库 + bind_models(db, [SkillFavorite]) + + def _ensure_tables(self) -> None: + self.db.create_tables([SkillFavorite]) + + @contextmanager + def atomic(self) -> Iterator[None]: + with self.db.atomic(): + yield + + def add(self, *, skill_name: str) -> bool: + """ + 添加收藏。 + 由于 skill_name 设定了 unique=True,使用 on_conflict_ignore() 避免重复插入报错。 + 返回 True 表示新增成功,False 表示已存在(未新增)。 + """ + self._ensure_tables() + + before = SkillFavorite.select().count() + # 插入数据,favorite_time 由数据库默认值处理 + SkillFavorite.insert({SkillFavorite.skill_name: skill_name}).on_conflict_ignore().execute() + after = SkillFavorite.select().count() + + return after > before + + def list(self) -> list[str]: + """ + 获取所有收藏的技能名。 + 按照收藏时间倒序排列(最新的在最前面)。 + """ + self._ensure_tables() + + q = ( + SkillFavorite + .select(SkillFavorite.skill_name) + .order_by(SkillFavorite.favorite_time.desc()) # 倒序:最新收藏的在前 + ) + return [row.skill_name for row in q] + + def delete(self, *, skill_name: str) -> bool: + """ + 删除收藏(硬删除)。 + 返回 True 表示删除成功,False 表示该技能本来就不存在。 + """ + self._ensure_tables() + + count = SkillFavorite.delete().where(SkillFavorite.skill_name == skill_name).execute() + return count > 0 From 7f62c24e3441331eeb3cd8a2f1f55063e6d58eea Mon Sep 17 00:00:00 2001 From: huangjunjia <2489441209@qq.com> Date: Wed, 25 Mar 2026 17:06:03 +0800 Subject: [PATCH 11/20] fix: update funtion name --- app/api/routes_skill.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/routes_skill.py b/app/api/routes_skill.py index 5890ff4..56780d9 100644 --- a/app/api/routes_skill.py +++ b/app/api/routes_skill.py @@ -454,7 +454,7 @@ def op(): @router.post("/skills/favorite/cancel") -async def skill_favorite_add(payload: SkillFavoriteCancelRequest, +async def skill_favorite_cancel(payload: SkillFavoriteCancelRequest, repo: SkillFavoriteRepository = Depends(get_skill_favorite_repo)): def op(): From ba621d8359bcc3acfb2a18e0f67ce4288e43e9a1 Mon Sep 17 00:00:00 2001 From: huangjunjia <2489441209@qq.com> Date: Wed, 25 Mar 2026 17:32:20 +0800 Subject: [PATCH 12/20] feat: add is_favorite --- app/api/routes_skill.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/app/api/routes_skill.py b/app/api/routes_skill.py index 56780d9..614fce7 100644 --- a/app/api/routes_skill.py +++ b/app/api/routes_skill.py @@ -324,33 +324,25 @@ def _put_once(k=key, d=desc, z=zh): await run_in_threadpool(_put_once) zh_by_md5[key] = zh - # 1. 假设这是你从 repo 获取到的收藏列表(按时间倒序:['skillA', 'skillB']) - # 请确保在实际代码中调用 repo 获取这个列表 + # 1. 获取收藏列表 favorite_skills = await run_in_threadpool( lambda: favorite_skill_repo.list() ) # --- 排序逻辑开始 --- - - # 构建一个优先级字典:{ "skill_name": 排序权重 } - # 收藏的技能按照列表顺序获得权重(0, 1, 2...),未收藏的统一给一个很大的权重(例如 99999) priority_map = {name: idx for idx, name in enumerate(favorite_skills)} def get_sort_key(item): name = item.get("name", "") if name in priority_map: - # 第一梯队:已收藏。保留收藏列表的顺序(index 越小越靠前) return (0, priority_map[name]) else: - # 第二梯队:未收藏。这里可以选择按名字字母排序,方便查阅 return (1, name) - # 先过滤掉非字典的数据,然后根据规则排序 sorted_items = sorted( [s for s in items if isinstance(s, dict)], key=get_sort_key ) - # --- 排序逻辑结束 --- return ok( @@ -364,15 +356,16 @@ def get_sort_key(item): if isinstance(s.get("description"), str) and s.get("description") else "", "source": "openclaw-bundled" if ( - isinstance(s.get("name"), str) and s.get("name") == "302ai-search") else ( + isinstance(s.get("name"), str) and s.get("name") == "302ai-search") else ( (s.get("source") or "") if isinstance(s.get("source"), str) else ""), "eligible": bool(s.get("eligible")) if "eligible" in s else None, "disabled": bool(s.get("disabled")) if "disabled" in s else None, "bundled": bool(s.get("bundled")) if "bundled" in s else None, "blockedByAllowlist": bool(s.get("blockedByAllowlist")) if "blockedByAllowlist" in s else None, "missing": s.get("missing") if isinstance(s.get("missing"), dict) else None, + "is_favorite": s.get("name") in priority_map, + } - # 这里改用排序后的 sorted_items for s in sorted_items ], "builtin_skills": [], From e267a5d0db0903ffd2cc0609196ae35a0a66bce3 Mon Sep 17 00:00:00 2001 From: huangjunjia <2489441209@qq.com> Date: Wed, 25 Mar 2026 19:09:21 +0800 Subject: [PATCH 13/20] chore: bump openclaw version to 2026.3.23-2 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 698c00b..d78c0f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ && apt-get install -y --no-install-recommends nodejs \ && npm install -g @anthropic-ai/claude-code@latest \ - && npm install -g openclaw@latest \ + && npm install -g openclaw@2026.3.23-2 \ && npm install -g clawhub@latest \ && npm install -g @playwright/cli@latest \ && apt-get clean \ From e42cd4be6163c5b4c4bbfb6a2db4d5f9bbaf44e9 Mon Sep 17 00:00:00 2001 From: huangjunjia <2489441209@qq.com> Date: Thu, 26 Mar 2026 17:26:12 +0800 Subject: [PATCH 14/20] fix: add sleep time for get skilll list --- app/api/routes_skill.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/api/routes_skill.py b/app/api/routes_skill.py index 614fce7..c01383a 100644 --- a/app/api/routes_skill.py +++ b/app/api/routes_skill.py @@ -1,5 +1,6 @@ from __future__ import annotations +import asyncio import base64 import hashlib import io @@ -251,6 +252,9 @@ async def skill_list( ): # 直接通过 openclaw CLI 获取 skill 列表(包含 source 等信息) oc_runner = CommandRunner() + + await asyncio.sleep(1) # fix 操作skill之后请求马上过来,OC还没刷新SKILL + oc_result = await oc_runner.exec_json("openclaw skills list --json") oc_skills: list[dict] = [] From 61855d3be07ab631e17a5ece2cd507ea0d2581a5 Mon Sep 17 00:00:00 2001 From: huangjunjia <2489441209@qq.com> Date: Thu, 26 Mar 2026 17:26:50 +0800 Subject: [PATCH 15/20] fix: create new session error --- app/api/routes_chat.py | 1 - app/api/routes_session.py | 44 +++++++++++++++++++-------------------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/app/api/routes_chat.py b/app/api/routes_chat.py index fe1fbe9..35fbb66 100644 --- a/app/api/routes_chat.py +++ b/app/api/routes_chat.py @@ -768,7 +768,6 @@ async def _run_openclaw_cmd(): if list_sessions_result.exit_code != 0: raise Exception(list_sessions_result.stderr) - try: data = json.loads(list_sessions_result.stdout) except json.decoder.JSONDecodeError: diff --git a/app/api/routes_session.py b/app/api/routes_session.py index 267541e..f76ca86 100644 --- a/app/api/routes_session.py +++ b/app/api/routes_session.py @@ -222,28 +222,28 @@ async def init_project(payload: ProjectInitRequest, repo: SessionRepository = De oc_agent_id = workspace_name - new_resp, list_sessions_result = await oc_new_session_and_list_active( - oc_agent_id=oc_agent_id, - runner=runner, - active=3, - ) - log_info(f"{new_resp}") - - if list_sessions_result.exit_code != 0: - return fail(list_sessions_result.stderr, status_code=400) - try: - data = json.loads(list_sessions_result.stdout) - except json.decoder.JSONDecodeError: - # fix openclaw 3.13 CLI --json没正确返回json格式 - data = await oc_load_sessions_json_as_list(oc_agent_name=oc_agent_id) - sessions = data.get("sessions", []) - - if not sessions: - return fail("No active sessions found", status_code=400) - - # 按 updatedAt 降序排序,取最新的一个 - latest_session = max(sessions, key=lambda s: s.get("updatedAt", 0)) - log_info(f"{latest_session}") + new_resp, list_sessions_result = await oc_new_session_and_list_active( + oc_agent_id=oc_agent_id, + runner=runner, + active=3, + ) + log_info(f"{new_resp}") + + if list_sessions_result.exit_code != 0: + return fail(list_sessions_result.stderr, status_code=400) + try: + data = json.loads(list_sessions_result.stdout) + except json.decoder.JSONDecodeError: + # fix openclaw 3.13 CLI --json没正确返回json格式 + data = await oc_load_sessions_json_as_list(oc_agent_name=oc_agent_id) + sessions = data.get("sessions", []) + + if not sessions: + return fail("No active sessions found", status_code=400) + + # 按 updatedAt 降序排序,取最新的一个 + latest_session = max(sessions, key=lambda s: s.get("updatedAt", 0)) + log_info(f"{latest_session}") await run_in_threadpool(lambda: repo.create_session( session_alias=payload.session_id, From 9e66772cbf41e2ee2dc491f7dc8374313cd4a99a Mon Sep 17 00:00:00 2001 From: huangjunjia <2489441209@qq.com> Date: Fri, 27 Mar 2026 14:56:48 +0800 Subject: [PATCH 16/20] feat: add manual_import_time --- app/api/routes_skill.py | 49 +++++++++++-- app/models/skill_manual.py | 19 +++++ app/repositories/skill_manual_import.py | 92 +++++++++++++++++++++++++ 3 files changed, 153 insertions(+), 7 deletions(-) create mode 100644 app/models/skill_manual.py create mode 100644 app/repositories/skill_manual_import.py diff --git a/app/api/routes_skill.py b/app/api/routes_skill.py index c01383a..e292f66 100644 --- a/app/api/routes_skill.py +++ b/app/api/routes_skill.py @@ -9,6 +9,7 @@ import shutil import tempfile import zipfile +from datetime import datetime from pathlib import Path from typing import Any, Optional from urllib.parse import quote @@ -36,6 +37,7 @@ from pydantic import BaseModel, Field from app.repositories.skill_favorite import SkillFavoriteRepository +from app.repositories.skill_manual_import import SkillManualImportRepository router = APIRouter() @@ -58,14 +60,21 @@ def get_skill_desc_cache_repo(db=Depends(get_db)) -> SkillDescZhCacheRepository: def get_skill_favorite_repo(db=Depends(get_db)) -> SkillFavoriteRepository: return SkillFavoriteRepository(db) +def get_skill_manual_import_repo(db=Depends(get_db)) -> SkillManualImportRepository: + return SkillManualImportRepository(db) + + @router.post("/skills") -async def create_skill(request: Request, repo: SkillDescZhCacheRepository = Depends(get_skill_desc_cache_repo)): +async def create_skill(request: Request, + repo: SkillDescZhCacheRepository = Depends(get_skill_desc_cache_repo), + manual_skill_repo: SkillManualImportRepository = Depends(get_skill_manual_import_repo)): """ 用户上传zip压缩包或者提供github链接,将数据先下载到临时文件,遍历寻找SKILL.md拷贝到实际保存位置 skill名字/描述通过解析SKILL.md里的yaml元信息获得 + :param manual_skill_repo: :param request: :param repo: :return: @@ -189,6 +198,11 @@ def cache_put_op(key=desc_key, desc=description, zh=description_zh): "description_zh": description_zh, }) + with manual_skill_repo.atomic(): + await run_in_threadpool( + lambda: manual_skill_repo.upsert(skill_name=name) + ) + await asyncio.sleep(1) # fix 操作skill之后请求马上过来,OC还没刷新SKILL return ok({"user_skills": skill_list}) @router.get("/skills/detail") @@ -248,13 +262,12 @@ async def skill_list( limit: int = Query(50, description="每页数量"), offset: int = Query(0, description="偏移量"), cache_repo: SkillDescZhCacheRepository = Depends(get_skill_desc_cache_repo), - favorite_skill_repo: SkillFavoriteRepository = Depends(get_skill_favorite_repo) + favorite_skill_repo: SkillFavoriteRepository = Depends(get_skill_favorite_repo), + manual_skill_repo: SkillManualImportRepository = Depends(get_skill_manual_import_repo) ): # 直接通过 openclaw CLI 获取 skill 列表(包含 source 等信息) oc_runner = CommandRunner() - await asyncio.sleep(1) # fix 操作skill之后请求马上过来,OC还没刷新SKILL - oc_result = await oc_runner.exec_json("openclaw skills list --json") oc_skills: list[dict] = [] @@ -333,15 +346,35 @@ def _put_once(k=key, d=desc, z=zh): lambda: favorite_skill_repo.list() ) + # 获取手动导入skills的时间信息 + manual_skills = await run_in_threadpool( + lambda: manual_skill_repo.list() + ) + # --- 排序逻辑开始 --- + # 收藏优先级:收藏列表中的顺序 priority_map = {name: idx for idx, name in enumerate(favorite_skills)} + # 手动导入时间映射:skill_name -> manual_import_time + manual_time_map = { + item["skill_name"]: item["manual_import_time"] + for item in manual_skills + } + + # 用于排序的零时间基准 + ZERO_TIME = datetime(1970, 1, 1) + def get_sort_key(item): name = item.get("name", "") if name in priority_map: - return (0, priority_map[name]) + # 第一优先级:收藏的 skill,按收藏顺序排列 + return (0, priority_map[name], ZERO_TIME) else: - return (1, name) + # 第二优先级:非收藏的 skill,按手动导入时间倒序排列(越新越靠前) + # 不存在手动导入时间的视为 ZERO_TIME,排在最后 + import_time = manual_time_map.get(name, ZERO_TIME) + # 取负时间戳实现倒序 + return (1, -import_time.timestamp() if import_time else 0, name) sorted_items = sorted( [s for s in items if isinstance(s, dict)], @@ -365,9 +398,11 @@ def get_sort_key(item): "eligible": bool(s.get("eligible")) if "eligible" in s else None, "disabled": bool(s.get("disabled")) if "disabled" in s else None, "bundled": bool(s.get("bundled")) if "bundled" in s else None, - "blockedByAllowlist": bool(s.get("blockedByAllowlist")) if "blockedByAllowlist" in s else None, + "blockedByAllowlist": bool( + s.get("blockedByAllowlist")) if "blockedByAllowlist" in s else None, "missing": s.get("missing") if isinstance(s.get("missing"), dict) else None, "is_favorite": s.get("name") in priority_map, + "manual_import_at": manual_time_map.get(s.get("name")).strftime("%Y-%m-%dT%H:%M:%S.%fZ") if manual_time_map.get(s.get("name")) else None, } for s in sorted_items diff --git a/app/models/skill_manual.py b/app/models/skill_manual.py new file mode 100644 index 0000000..d556e38 --- /dev/null +++ b/app/models/skill_manual.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +import datetime +import hashlib + +from peewee import AutoField, CharField, DateTimeField, TextField, BooleanField + +from app.models.base import BaseModel + +class SkillManualImport(BaseModel): + id = AutoField() + skill_name = CharField(max_length=64, unique=True) + manual_import_time = DateTimeField(default=datetime.datetime.now) + + class Meta: + table_name = "skill_manual_import" + indexes = ( + (('manual_import_time',), False), + ) diff --git a/app/repositories/skill_manual_import.py b/app/repositories/skill_manual_import.py new file mode 100644 index 0000000..05247e0 --- /dev/null +++ b/app/repositories/skill_manual_import.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +import datetime +from contextlib import contextmanager +from typing import Iterator + +from peewee import SqliteDatabase, EXCLUDED + +from app.models.base import bind_models +from app.models.skill_manual import SkillManualImport + +class SkillManualImportRepository: + def __init__(self, db: SqliteDatabase): + self.db = db + # 绑定模型到数据库 + bind_models(db, [SkillManualImport]) + + def _ensure_tables(self) -> None: + self.db.create_tables([SkillManualImport]) + + @contextmanager + def atomic(self) -> Iterator[None]: + with self.db.atomic(): + yield + + import datetime + + def upsert(self, *, skill_name: str) -> bool: + """ + 添加或更新收藏。 + skill_name 不存在则插入,已存在则更新 manual_import_time。 + 返回 True 表示新增,False 表示已存在(仅更新了时间)。 + """ + self._ensure_tables() + + now = datetime.datetime.now() + + # 尝试查找已有记录 + existing = ( + SkillManualImport + .select() + .where(SkillManualImport.skill_name == skill_name) + .first() + ) + + if existing: + # 已存在,更新时间 + ( + SkillManualImport + .update({SkillManualImport.manual_import_time: now}) + .where(SkillManualImport.skill_name == skill_name) + .execute() + ) + return False + else: + # 不存在,插入 + SkillManualImport.create( + skill_name=skill_name, + manual_import_time=now, + ) + return True + + def list(self) -> list[dict]: + """ + 获取所有收藏的技能。 + 按照收藏时间倒序排列(最新的在最前面)。 + 返回包含 skill_name 和 manual_import_time 的字典列表。 + """ + self._ensure_tables() + + q = ( + SkillManualImport + .select(SkillManualImport.skill_name, SkillManualImport.manual_import_time) + .order_by(SkillManualImport.manual_import_time.desc()) + ) + return [ + { + "skill_name": row.skill_name, + "manual_import_time": row.manual_import_time, + } + for row in q + ] + + def delete(self, *, skill_name: str) -> bool: + """ + 删除收藏(硬删除)。 + 返回 True 表示删除成功,False 表示该技能本来就不存在。 + """ + self._ensure_tables() + + count = SkillManualImport.delete().where(SkillManualImport.skill_name == skill_name).execute() + return count > 0 From 89a857ea5bf4c49ff0669a785f73b3032cc40090 Mon Sep 17 00:00:00 2001 From: huangjunjia <2489441209@qq.com> Date: Fri, 27 Mar 2026 15:54:18 +0800 Subject: [PATCH 17/20] feat: add favorite_at --- app/api/routes_skill.py | 23 ++++++++++++++++++----- app/repositories/skill_favorite.py | 12 +++++++++--- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/app/api/routes_skill.py b/app/api/routes_skill.py index e292f66..ac408e3 100644 --- a/app/api/routes_skill.py +++ b/app/api/routes_skill.py @@ -352,8 +352,17 @@ def _put_once(k=key, d=desc, z=zh): ) # --- 排序逻辑开始 --- - # 收藏优先级:收藏列表中的顺序 - priority_map = {name: idx for idx, name in enumerate(favorite_skills)} + # 收藏优先级:按收藏时间倒序排列(repo 层已保证顺序),构建 name -> idx 映射 + priority_map = { + item["skill_name"]: idx + for idx, item in enumerate(favorite_skills) + } + + # 收藏时间映射:skill_name -> favorite_time + favorite_time_map = { + item["skill_name"]: item["favorite_time"] + for item in favorite_skills + } # 手动导入时间映射:skill_name -> manual_import_time manual_time_map = { @@ -373,7 +382,6 @@ def get_sort_key(item): # 第二优先级:非收藏的 skill,按手动导入时间倒序排列(越新越靠前) # 不存在手动导入时间的视为 ZERO_TIME,排在最后 import_time = manual_time_map.get(name, ZERO_TIME) - # 取负时间戳实现倒序 return (1, -import_time.timestamp() if import_time else 0, name) sorted_items = sorted( @@ -402,8 +410,13 @@ def get_sort_key(item): s.get("blockedByAllowlist")) if "blockedByAllowlist" in s else None, "missing": s.get("missing") if isinstance(s.get("missing"), dict) else None, "is_favorite": s.get("name") in priority_map, - "manual_import_at": manual_time_map.get(s.get("name")).strftime("%Y-%m-%dT%H:%M:%S.%fZ") if manual_time_map.get(s.get("name")) else None, - + # 新增:收藏时间 + "favorite_at": favorite_time_map[s.get("name")].strftime("%Y-%m-%dT%H:%M:%S.%fZ") + if s.get("name") in favorite_time_map and favorite_time_map.get(s.get("name")) + else None, + "manual_import_at": manual_time_map.get(s.get("name")).strftime("%Y-%m-%dT%H:%M:%S.%fZ") + if manual_time_map.get(s.get("name")) + else None, } for s in sorted_items ], diff --git a/app/repositories/skill_favorite.py b/app/repositories/skill_favorite.py index 647ea8a..05b64d0 100644 --- a/app/repositories/skill_favorite.py +++ b/app/repositories/skill_favorite.py @@ -38,7 +38,7 @@ def add(self, *, skill_name: str) -> bool: return after > before - def list(self) -> list[str]: + def list(self) -> list[dict]: """ 获取所有收藏的技能名。 按照收藏时间倒序排列(最新的在最前面)。 @@ -47,10 +47,16 @@ def list(self) -> list[str]: q = ( SkillFavorite - .select(SkillFavorite.skill_name) + .select(SkillFavorite.skill_name, SkillFavorite.favorite_time) .order_by(SkillFavorite.favorite_time.desc()) # 倒序:最新收藏的在前 ) - return [row.skill_name for row in q] + return [ + { + "skill_name": row.skill_name, + "favorite_time": row.favorite_time, + } + for row in q + ] def delete(self, *, skill_name: str) -> bool: """ From ef721845febeda06436fdb28dde0a2424fc9fc18 Mon Sep 17 00:00:00 2001 From: huangjunjia <2489441209@qq.com> Date: Fri, 27 Mar 2026 16:33:52 +0800 Subject: [PATCH 18/20] fix(regex): exclude directory from file matching --- app/api/routes_chat.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/api/routes_chat.py b/app/api/routes_chat.py index 35fbb66..69477ab 100644 --- a/app/api/routes_chat.py +++ b/app/api/routes_chat.py @@ -846,7 +846,8 @@ async def _run_openclaw_cmd(): prefix = "\n\n".join(prefix_parts) + "\n\n" final_user_prompt = prefix + user_prompt collected_text_chunks: list[str] = [] - file_regex = re.compile(r"^- `?(\/[\S`]+)`?", re.MULTILINE) + # 确保扩展名在路径最后一个 / 之后 + file_regex = re.compile(r"^- `?(\/\S+\/[^/\s]+\.[^/\s`]+)`?$", re.MULTILINE) async for event in oc_chat_completions_sse( oc_session_key=oc_session_key, From 3c1d5d1f166d54437f2a3da348d5281f6d4de685 Mon Sep 17 00:00:00 2001 From: huangjunjia <2489441209@qq.com> Date: Fri, 27 Mar 2026 17:29:26 +0800 Subject: [PATCH 19/20] fix: validate file path exist before return result --- app/api/routes_chat.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/api/routes_chat.py b/app/api/routes_chat.py index 69477ab..c8a3197 100644 --- a/app/api/routes_chat.py +++ b/app/api/routes_chat.py @@ -860,10 +860,14 @@ async def _run_openclaw_cmd(): combined = "".join(collected_text_chunks) matches = file_regex.findall(combined) if matches: + # 校验路径是否存在,只保留存在的 + valid_matches = [m for m in matches if os.path.isfile(m)] + log_warning(f"{matches}") + log_warning(f"{valid_matches}") deploy_check_info = json.dumps({ "type": "result", "is_error": False, - "result": "\n".join(f"-`{m}`"for m in matches), + "result": "\n".join(f"-`{m}`" for m in valid_matches) if valid_matches else "", }, ensure_ascii=False) chunk = gpt_stream_chunk(deploy_check_info) yield f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n".encode("utf-8") From a35ff4137836957db88a45ec5b9f282958656ff4 Mon Sep 17 00:00:00 2001 From: huangjunjia <2489441209@qq.com> Date: Fri, 27 Mar 2026 18:14:39 +0800 Subject: [PATCH 20/20] fix: add default result type SSE response --- app/api/routes_chat.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/api/routes_chat.py b/app/api/routes_chat.py index c8a3197..c703ea4 100644 --- a/app/api/routes_chat.py +++ b/app/api/routes_chat.py @@ -862,15 +862,19 @@ async def _run_openclaw_cmd(): if matches: # 校验路径是否存在,只保留存在的 valid_matches = [m for m in matches if os.path.isfile(m)] - log_warning(f"{matches}") - log_warning(f"{valid_matches}") - deploy_check_info = json.dumps({ + diff_file_check_info = json.dumps({ "type": "result", "is_error": False, "result": "\n".join(f"-`{m}`" for m in valid_matches) if valid_matches else "", }, ensure_ascii=False) - chunk = gpt_stream_chunk(deploy_check_info) - yield f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n".encode("utf-8") + else: + diff_file_check_info = json.dumps({ + "type": "result", + "is_error": False, + "result": "", + }, ensure_ascii=False) + diff_file_chunk = gpt_stream_chunk(diff_file_check_info) + yield f"data: {json.dumps(diff_file_chunk, ensure_ascii=False)}\n\n".encode("utf-8") if payload.enable_pre_deploy_check: pre_deploy_check_result = await runner.exec_json(command=check_cmd, cwd=workspace_path)