Skill: liuchuan01/ais-skill (version 1.0.1)

--- name: kb-skill description: 通过 TorchV AIS 知识库远端与用户协作使用这个技能，可进行的操作包括阅读结构、搜索和读取文档、创建文档或目录、修改文档、发布或返回可跳转的文档链接时。需要利用脚本将 `kb ...` 命令发送到远端 AIS 执行。 --- # KB Skill 你拥有一个内置的知识库AIS(通过kb命令操作)。你的职责是精确完成当前 KB 任务，除非用户要求，不要大规模进行改动。 AIS 是一个**持久化、版本化的Knowledge Base** AIS 中的操作会真实生效，且用户也会通过UI使用。可能因为权限、并发或约束失败。 通过这个技能操作远端 KB/AIS 时，不要假设本机存在 `kb` 可执行文件。普通 `kb ...` 命令始终交给 [scripts/kb_execute.py](./scripts/kb_execute.py) 发送到 KB 执行服务；文件上传、文件直下载、生成下载链接属于特殊能力，走脚本内置的文件接口，不走 `kb ...` 字符串。 ## 术语约定： - “AIS / 知识库 / KB”默认都指知识库本身 - AIS 中存储的是知识内容： `仓库` 包含 `目录` 包含 `文档`,每个都有稳定唯一标识（code），但可能存在同name。 - `仓库`：别名空间、space、container、知识库,工作区也是一种仓库，一般用户在工作区工作。 - `目录`：别名文件夹、folder，视为一种特殊的文档，没有内容。 - `文档`：别名页面、Page、Wiki，有多种格式如pdf、md、excel。但无论哪种，你阅读到的都是AIS解析后的干净文本。 ## 配置执行环境 优先使用环境变量： ```bash export KB_EXECUTE_URL="https://bot.torchv.com" export KB_TOKEN="xxxxxx" ``` 也可以在调用时显式传参如下： ```bash ./scripts/kb_execute.py --url "https://bot.torchv.com" --token "xxxxxx" "kb cat --code 2031630406710923264" ``` 如果命令较长、包含多行 patch，优先把完整命令写入本地 UTF-8 文本文件，再用 `--command-file` 发送： ```bash ./scripts/kb_execute.py --command-file /tmp/kb-command.txt ``` > 若无 token，直接向用户索要，并说明可以在已登录 AIS 页面里打开开发者工具，找到一个知识库请求，从请求头中复制 `token` 或 `Authorization` 的值。 默认请求体为： ```json { "command": "kb <command> [args]" } ``` 并自动带上请求头： ```text Content-Type: application/json [REDACTED_SECRET] ``` 如还需要其他头，追加 `--header key=value`。 ## 运行命令 把真实 KB 命令整体作为一个字符串传给脚本： ```bash ./scripts/kb_execute.py "kb ls" ./scripts/kb_execute.py "kb search --query \"发布流程\" --repo-code TEAM_DOCS --mode hybrid --topk 10" ./scripts/kb_execute.py "kb cat --code 2031630406710923264" ./scripts/kb_execute.py "kb render 2031630406710923264" ``` 脚本会向执行服务发起 POST 请求并输出响应；如果服务返回 JSON，脚本会格式化打印。命令失败后，不要假设 KB 状态已经变化，先重新读取确认。 文件相关能力直接走脚本内置参数，不走 `kb ...` 字符串： ```bash ./scripts/kb_execute.py --upload-file ./demo.pdf --path-name "/团队空间/附件" ./scripts/kb_execute.py --download-code 2031630406710923264 --save-to /tmp/ ./scripts/kb_execute.py --download-code 2031630406710923264 --extract-is true --save-to /tmp/result.md ./scripts/kb_execute.py --download-link-code 2031630406710923264 --extract-is false ``` 如果 `--download-code` 返回的是 JSON 错误而不是文件内容，脚本会直接失败退出。此时优先改用 `--download-link-code` 生成票据，再用 `wget` 或 `curl` 验证下载链路。 ## 执行前检查 - 先确认 `KB_EXECUTE_URL` 和 `KB_TOKEN` 已提供，或本次调用显式传入 `--url` 和 `--token`。 - 如果缺少执行地址或 token，立即停止并向用户索取，不要继续猜测或尝试 KB 命令。 - 多行命令、复杂 HTML 或 patch 优先走 `--command-file`，避免 shell 转义破坏命令正文。 ## 默认工作流 0. 先打开[references/read.md](./references/read.md)了解阅读、搜索相关能力。 1. 先定位范围：用 `kb ls`、`kb tree` 或 `kb search`。 2. 锁定唯一对象后，优先使用稳定 `code`。 3. 任何修改前，先执行 `kb cat` 读取当前内容，后按需阅读修改或写入文档。 4. 选择满足需求的最小操作，不要默认整篇重写。 5. 如果最终答复需要 KB 链接，最后再执行 `kb render`。 ## 全局规则 - 仓库、目录、文档都可能同名；不唯一时先消歧，不要猜。 - 后续分析和修改以 `kb cat` 返回内容为准。 - `kb write` 或 `kb edit --patch` 写入的正文优先是 HTML，不是 Markdown 正文。 - 只有在明确不存在目标文档时才新建。 - 修改已有文档时优先局部变更。 - 删除、移动、发布属于高风险操作，只有用户明确要求时才执行。 - 处理 `.pdf`、`.xlsx`、`.docx` 等不可直接编辑格式时，默认先读取内容再创建语义等价的 `.md` 文档，除非工具明确支持原格式编辑。 ## 按需加载的参考资料 - 读取、搜索、定位和渲染链接：看 [references/read.md](./references/read.md) - 新建文档、目录、移动、删除、发布：看 [references/write.md](./references/write.md) - 对已有文档做 patch 精确修改：看 [references/update.md](./references/update.md) - 文件上传、下载：看 [references/file-transfer.md](./references/file-transfer.md)

path: scripts/kb_execute.py
content: #!/usr/bin/env python3
"""Execute KB commands through the remote HTTP execute service."""

from __future__ import annotations

import argparse
import mimetypes
import json
import os
import sys
import uuid
import urllib.error
import urllib.parse
import urllib.request


EXECUTE_PATH = "/kb/atomix/execute"


def parse_header(raw_header: str) -> tuple[str, str]:
    if "=" in raw_header:
        key, value = raw_header.split("=", 1)
    elif ":" in raw_header:
        key, value = raw_header.split(":", 1)
    else:
        raise ValueError(f"invalid header format: {raw_header!r}")

    key = key.strip()
    value = value.strip()
    if not key:
        raise ValueError(f"header key is empty: {raw_header!r}")
    return key, value


def print_response(body: bytes) -> None:
    text = body.decode("utf-8", errors="replace")
    try:
        parsed = json.loads(text)
    except json.JSONDecodeError:
        print(text)
        return

    print(json.dumps(parsed, ensure_ascii=False, indent=2))


def parse_json_body(body: bytes) -> object | None:
    try:
        return json.loads(body.decode("utf-8"))
    except (UnicodeDecodeError, json.JSONDecodeError):
        return None


def read_command_file(path: str) -> str:
    with open(path, "r", encoding="utf-8") as handle:
        return handle.read()


def normalize_execute_url(raw_url: str) -> str:
    parsed = urllib.parse.urlparse(raw_url)
    if not parsed.scheme or not parsed.netloc:
        raise ValueError(f"invalid KB execute URL: {raw_url!r}")
    if parsed.path == EXECUTE_PATH:
        return urllib.parse.urlunparse(parsed)
    if parsed.path in ("", "/"):
        return urllib.parse.urlunparse(parsed._replace(path=EXECUTE_PATH, params="", query="", fragment=""))
    return urllib.parse.urlunparse(parsed)


def build_auth_headers(token: str) -> dict[str, str]:
    normalized = token.strip()
    if normalized.startswith("tk"):
        return {"Authorization": f"Bearer {normalized}"}
    if normalized.startswith("Bearer"):
        return {"Authorization": f"{normalized}"}
    return {"token": normalized}


def build_file_url(base_url: str, endpoint_path: str, **query: str) -> str:
    from urllib.parse import urlencode, urljoin

    url = urljoin(base_url, endpoint_path)
    if query:
        return f"{url}?{urlencode(query)}"
    return url


def encode_multipart(fields: dict[str, str], file_field: str, file_name: str, file_bytes: bytes, content_type: str) -> tuple[bytes, str]:
    boundary = f"----kbatomix{uuid.uuid4().hex}"
    chunks: list[bytes] = []

    for key, value in fields.items():
        chunks.extend(
            [
                f"--{boundary}\r\n".encode("utf-8"),
                f'Content-Disposition: form-data; name="{key}"\r\n\r\n'.encode("utf-8"),
                value.encode("utf-8"),
                b"\r\n",
            ]
        )

    chunks.extend(
        [
            f"--{boundary}\r\n".encode("utf-8"),
            f'Content-Disposition: form-data; name="{file_field}"; filename="{file_name}"\r\n'.encode("utf-8"),
            f"Content-Type: {content_type}\r\n\r\n".encode("utf-8"),
            file_bytes,
            b"\r\n",
            f"--{boundary}--\r\n".encode("utf-8"),
        ]
    )
    return b"".join(chunks), boundary


def upload_file(url: str, token: str, headers: dict[str, str], local_path: str, path_name: str, file_name: str | None, timeout: float) -> int:
    if not os.path.isfile(local_path):
        print(f"upload source is not a file: {local_path}", file=sys.stderr)
        return 1

    actual_name = file_name or os.path.basename(local_path)
    content_type = mimetypes.guess_type(actual_name)[0] or "application/octet-stream"
    with open(local_path, "rb") as handle:
        file_bytes = handle.read()

    payload, boundary = encode_multipart(
        {"fileName": actual_name, "pathName": path_name},
        "file",
        actual_name,
        file_bytes,
        content_type,
    )
    request = urllib.request.Request(
        url=build_file_url(url, "/openapi/partner/sage/file/upload"),
        data=payload,
        headers={
            **headers,
            **build_auth_headers(token),
            "Content-Type": f"multipart/form-data; boundary={boundary}",
        },
        method="POST",
    )
    return send_request(request, timeout)


def download_file(
    url: str,
    token: str,
    headers: dict[str, str],
    code: str,
    local_path: str,
    extract_is: bool | None,
    overwrite: bool,
    timeout: float,
) -> int:
    query: dict[str, str] = {"code": code}
    if extract_is is not None:
        query["extractIs"] = "true" if extract_is else "false"

    request = urllib.request.Request(
        url=build_file_url(url, "/openapi/partner/sage/file/download", **query),
        headers={**headers, **build_auth_headers(token)},
        method="GET",
    )

    try:
        with urllib.request.urlopen(request, timeout=timeout) as response:
            body = response.read()
            parsed = parse_json_body(body)
            if isinstance(parsed, dict):
                print(json.dumps(parsed, ensure_ascii=False, indent=2), file=sys.stderr)
                return 1
            target_path = resolve_download_path(local_path, response.headers.get("Content-Disposition"), code, overwrite)
            os.makedirs(os.path.dirname(target_path) or ".", exist_ok=True)
            mode = "wb" if overwrite else "xb"
            with open(target_path, mode) as handle:
                handle.write(body)
            print(
                json.dumps(
                    {
                        "savedPath": target_path,
                        "fileName": os.path.basename(target_path),
                        "size": len(body),
                        "contentType": response.headers.get("Content-Type", "application/octet-stream"),
                    },
                    ensure_ascii=False,
                    indent=2,
                )
            )
            return 0
    except FileExistsError:
        print(f"download target already exists: {local_path}", file=sys.stderr)
        return 1
    except urllib.error.HTTPError as exc:
        print(f"HTTP {exc.code}", file=sys.stderr)
        print_response(exc.read())
        return 1
    except urllib.error.URLError as exc:
        print(f"request failed: {exc.reason}", file=sys.stderr)
        return 1


def get_download_link(
    url: str,
    token: str,
    headers: dict[str, str],
    code: str,
    extract_is: bool | None,
    timeout: float,
) -> int:
    query: dict[str, str] = {"code": code}
    if extract_is is not None:
        query["extractIs"] = "true" if extract_is else "false"

    request = urllib.request.Request(
        url=build_file_url(url, "/openapi/partner/sage/file/downloadLink", **query),
        headers={**headers, **build_auth_headers(token)},
        method="GET",
    )
    return send_request(request, timeout)


def resolve_download_path(local_path: str, content_disposition: str | None, code: str, overwrite: bool) -> str:
    target_path = local_path
    if os.path.isdir(local_path):
        target_path = os.path.join(local_path, extract_download_name(content_disposition) or code)
    if not overwrite and os.path.exists(target_path):
        raise FileExistsError(target_path)
    return target_path


def extract_download_name(content_disposition: str | None) -> str | None:
    if not content_disposition:
        return None
    import re
    from urllib.parse import unquote

    utf8_match = re.search(r"filename\\*=UTF-8''([^;]+)", content_disposition, re.IGNORECASE)
    if utf8_match:
        return unquote(utf8_match.group(1))
    quoted_match = re.search(r'filename="([^"]+)"', content_disposition, re.IGNORECASE)
    if quoted_match:
        return quoted_match.group(1)
    plain_match = re.search(r"filename=([^;]+)", content_disposition, re.IGNORECASE)
    if plain_match:
        return plain_match.group(1).strip()
    return None


def send_request(request: urllib.request.Request, timeout: float) -> int:
    try:
        with urllib.request.urlopen(request, timeout=timeout) as response:
            print_response(response.read())
            return 0
    except urllib.error.HTTPError as exc:
        print(f"HTTP {exc.code}", file=sys.stderr)
        print_response(exc.read())
        return 1
    except urllib.error.URLError as exc:
        print(f"request failed: {exc.reason}", file=sys.stderr)
        return 1


def main() -> int:
    parser = argparse.ArgumentParser(
        description="Send a KB command string to the remote execute endpoint.",
    )
    parser.add_argument(
        "command",
        nargs="?",
        help='KB command string, for example: "kb cat --code 2031630406710923264"',
    )
    parser.add_argument(
        "--command",
        dest="command_flag",
        help='KB command string, for example: "kb ls"',
    )
    parser.add_argument(
        "--command-file",
        help="UTF-8 text file containing the KB command string.",
    )
    parser.add_argument(
        "--upload-file",
        help="Upload a local file from this machine to AIS.",
    )
    parser.add_argument(
        "--download-code",
        help="Download an AIS file by code.",
    )
    parser.add_argument(
        "--download-link-code",
        help="Generate an AIS file download link by code.",
    )
    parser.add_argument(
        "--save-to",
        help="Local file path or directory for --download-code.",
    )
    parser.add_argument(
        "--path-name",
        help="Remote AIS path name for --upload-file.",
    )
    parser.add_argument(
        "--file-name",
        help="Optional override file name for --upload-file.",
    )
    parser.add_argument(
        "--extract-is",
        choices=["true", "false"],
        help="Optional extractIs query flag for --download-code or --download-link-code.",
    )
    parser.add_argument(
        "--overwrite",
        action="store_true",
        help="Allow overwriting existing local files for --download-code.",
    )
    parser.add_argument(
        "--timeout",
        type=float,
        default=30.0,
        help="Request timeout in seconds. Default: 30.",
    )
    parser.add_argument(
        "--url",
        default=os.environ.get("KB_EXECUTE_URL"),
        help="AIS site URL or full KB execute endpoint. Defaults to env KB_EXECUTE_URL.",
    )
    parser.add_argument(
        "--token",
        default=os.environ.get("KB_TOKEN"),
        help="KB token header value. Defaults to env KB_TOKEN.",
    )
    parser.add_argument(
        "--header",
        action="append",
        default=[],
        help="Additional request header in key=value or key:value format.",
    )
    args = parser.parse_args()

    modes = [
        bool(args.command),
        bool(args.command_flag),
        bool(args.command_file),
        bool(args.upload_file),
        bool(args.download_code),
        bool(args.download_link_code),
    ]
    if sum(modes) != 1:
        parser.error(
            "choose exactly one operation: command / --command / --command-file / --upload-file / --download-code / --download-link-code"
        )
    if not args.url:
        parser.error("missing KB execute URL; pass --url or set KB_EXECUTE_URL")
    if not args.[REDACTED_SECRET]"missing KB token; pass --token or set KB_TOKEN")

    try:
        args.url = normalize_execute_url(args.url)
    except ValueError as exc:
        parser.error(str(exc))

    headers: dict[str, str] = {}
    try:
        for raw_header in args.header:
            key, value = parse_header(raw_header)
            headers[key] = value
    except ValueError as exc:
        parser.error(str(exc))

    extract_is: bool | None = None
    if args.extract_is is not None:
        extract_is = args.extract_is == "true"

    if args.upload_file:
        if not args.path_name:
            parser.error("--upload-file requires --path-name")
        return upload_file(args.url, args.token, headers, args.upload_file, args.path_name, args.file_name, args.timeout)

    if args.download_code:
        if not args.save_to:
            parser.error("--download-code requires --save-to")
        return download_file(
            args.url,
            args.token,
            headers,
            args.download_code,
            args.save_to,
            extract_is,
            args.overwrite,
            args.timeout,
        )

    if args.download_link_code:
        return get_download_link(args.url, args.token, headers, args.download_link_code, extract_is, args.timeout)

    command = args.command_flag or args.command
    if args.command_file:
        command = read_command_file(args.command_file)
    if not command:
        parser.error("missing KB command string")

    headers = {"Content-Type": "application/json", **build_auth_headers(args.token), **headers}
    payload = json.dumps({"command": command}, ensure_ascii=False).encode("utf-8")
    request = urllib.request.Request(
        url=args.url,
        data=payload,
        headers=headers,
        method="POST",
    )

    return send_request(request, args.timeout)


if __name__ == "__main__":
    raise SystemExit(main())


path: references/file-transfer.md
content: # 文件上传/下载能力说明

这些能力与普通 `kb ...` 命令不同，原因是它们需要处理二进制文件、本地文件系统，或返回可直接wget下载的临时链接。

## 1. Agent 入参

### 上传
本地文件上传到AIS

- `localPath`：MCP server / Python 脚本所在机器上的真实文件路径
- `pathName`：AIS 知识库中的目标目录
- `fileName`：可选；若不传，程序会取 `localPath` 的文件名

### 下载
AIS文件下载到本地

- `code`：远端文件或文档的稳定标识
- `localPath`：MCP server / Python 脚本所在机器上的真实保存路径
- `extractIs`：可选；是则下载提取清洗后的 md 内容，否则下载原文件
- `overwrite`：可选；默认不覆盖已有文件

### 生成下载链接
生成可wget下载文件的临时链接

- `code`：远端文件或文档的稳定标识
- `extractIs`：可选；是则下载提取清洗后的 md 内容，否则下载原文件

path: references/read.md
content: # KB 读取操作手册

从这个技能执行命令时，把下面的 `kb ...` 命令字符串整体传给 `./scripts/kb_execute.py`，例如：`./scripts/kb_execute.py "kb cat --code DOC_12345"`。
  
当你需要定位仓库、查看目录、搜索内容、读取文档或返回文档链接时，读这个文件。  
  
## 工作流  
  
1. 不知道仓库或目录时，先用 `kb ls`。  
2. 需要看目录层级时，用 `kb tree`，不要拿它代替内容搜索。  
3. 知道主题但不知道具体文件时，用 `kb search`。  
4. 锁定到具体文档后，用 `kb cat` 读取。  
5. 只有在最终答复需要可点击链接时，才用 `kb render`。  
 
## 共享规则  
  
- 一旦拿到仓库或文档 `code`，优先使用 `code`。  
- 路径命中不唯一时，先消歧，再进入修改流程。  
- `kb cat` 返回内容就是后续理解和修改的依据，包括其中的 HTML 结构。  
  
## 命令卡片  
  
### `kb ls`  
  
用于列出可访问仓库，或者列出某个路径下的子项。  
  
- 常用写法：  
  - `kb ls`  
  - `kb ls <repo_or_path>`  
- 常用参数：  
  - `--sort name|updated|size`  
  - `--limit N`  
- 示例：  
  
```text  
kb ls team-workspace/docs --sort updated --limit 50  
```  
  
### `kb tree`  
  
用于在读取前快速理解目录结构。  
  
- 常用写法：  
  - `kb tree <repo/path/> [--depth N] [--limit N]`  
- 适合看层级，不适合替代全文搜索。  
- 示例：  
  
```text  
kb tree team-workspace/docs/project-a/ --depth 2  
```  
  
### `kb search`  
  
用于按主题搜索内容，尤其适合“知道要找什么，但不知道文件名”的场景。  
  
- 常用写法：  
  - `kb search --query "<text>" --repo-code <repo_code>`  
- 常用过滤：  
  - `--path <prefix>`  
  - `--file <glob>`  
  - `--doc-code <code>` 或 `--codes <a,b,c>`  
  - `--mode lex|sem|hybrid`  
  - `--topk N`  
- 示例：  
  
```text  
kb search --query "发布流程" --repo-code TEAM_DOCS --path process/ --mode hybrid --topk 10  
```  
  
### `kb cat`  
  
用于读取你准备摘要、引用或修改的具体文档。  
  
- 常用写法：  
  - `kb cat --code <doc_code>`  
  - `kb cat --name <repo/path.md>`  
- 常用参数：  
  - `--range A:B`  
  - `--head N`  
  - `--tail N`  
  - `-n`  
- 使用要求：  
  - 修改前必须先读。  
  - 后续 patch 内容要基于返回的 HTML。  
- 示例：  
  
```text  
kb cat --code DOC_12345 -n  
```  
  
### `kb render`  
  
用于在最后把文档 `code` 转成可点击链接。  
  
- 常用写法：  
  - `kb render <doc_code,doc_code,...>`  
- 只渲染最终答复里确实需要引用的文档。  
- 示例：  
  
```text  
kb render DOC_12345,DOC_67890  
```  
  
## 不在本文件覆盖的内容

- 新建文档
- 新建目录、移动、删除、发布
- 基于 patch 的精确修改

需要写入或修改时，直接按需阅读本 skill 自带的参考资料：

- 新建与结构操作看 [write.md](./write.md)
- 基于 patch 的精确修改看 [update.md](./update.md)


path: references/write.md
content: # KB 新建与结构操作

从这个技能执行命令时，把下面的 `kb ...` 命令字符串整体传给 `./scripts/kb_execute.py`，例如：`./scripts/kb_execute.py "kb write --name repo/path.md --content '<h1>Title</h1>'"`。如果正文 HTML 很长或包含复杂转义，优先把完整命令写入本地 UTF-8 文本文件，再通过 `--command-file` 发送。

当你需要新建文档、新建目录、移动内容、删除内容或发布内容时，读这个文件。

## 工作流

1. 先确认仓库、路径和目标对象。
2. 涉及已有文档时，先读当前内容再决定操作。
3. 选择能满足需求的最小变更。
4. 删除、移动、发布都按“用户明确要求才执行”处理。
5. 如果最终答复需要指向结果文档，再补 `kb render`。

## 共享规则

- `kb write` 只负责新建，不负责覆盖。
- 通过 `kb write` 写入的正文必须是 HTML。
- 用户要求 `.pdf`、`.xlsx`、`.docx` 等不可直接编辑格式时，先读取原内容作参考，再创建语义等价的 `.md`，正文用 HTML 表达；只有工具明确支持原格式编辑时，才直接改原文件。
- 如果目标可能已存在，先用读取类命令确认。
- 如果目标不唯一，不要猜，先确认。

## 命令卡片

### `kb write`

用于创建新文档。

- 常用写法：
  - `kb write --name <repo/path.md> --content '<html...>'`
  - `kb write --repo-code <code> --path <path.md> --content '<html...>'`
- 使用要求：
  - 只在确认目标文档不存在时使用。
  - 直接写 HTML，不要先写 Markdown 再转换。
  - 表格类内容使用 HTML 表格标签。
- 示例：

```text
kb write --name team-workspace/reports/q1-summary.md --content '<h1>Q1 Summary</h1><table><thead><tr><th>Region</th><th>Amount</th></tr></thead><tbody><tr><td>CN</td><td>120</td></tr></tbody></table>' --status publish
```

### `kb mkdir`

用于按需创建目录。

- 常用写法：
  - `kb mkdir <repo/dir/>`
  - `kb mkdir --repo-code <code> --path <dir/>`
- 常用参数：
  - `--parents`
  - `--exist-ok true|false`
- 示例：

```text
kb mkdir --repo-code TEAM_DOCS --path reports/2026/ --parents
```
### kb cp

用于复制知识库对象到目标位置。只有用户明确要求“复制/拷贝”时才使用。

- 常用写法：
    - `kb cp --from <repo/src.md> --to <repo/dst.md>`
    - `kb cp --doc-code <DOC_CODE> --to <repo/dst.md>`
    - `kb cp --from <repo/src_dir/> --to <repo/dst_dir/> --recursive true`
- 常用参数：
    - `--from <repo/path>`：按知识库路径指定复制源
    - `--doc-code <DOC_CODE>`：按元素编码指定复制源
    - `--to <repo/path>`：目标路径，必填
    - `--recursive true|false`：复制目录时是否连同子内容一起复制；默认 false
- 行为语义：
    - 复制不会删除或移动源对象。
    - 目标路径中的父目录不存在时，会自动补建目录。
    - 复制普通对象成功后，目标对象通常会生成新的 code。
    - 返回结果里的 type 目前是 element 或 directory。
- 限制与冲突策略：
    - 必须二选一提供 `--from` 或 `--doc-code`，不能同时传，也不能都不传。
    - 目录复制建议显式传 --recursive true。

示例：

```text
kb cp --doc-code DOC_12345 --to team-workspace/reports/q1-summary-copy.md

kb cp --from team-workspace/project-a/ --to archive/project-a/ --recursive true

```

### `kb mv`

用于移动或重命名文档、目录。只有用户明确要求时才使用。

- 常用写法：
  - `kb mv --from <src> --to <dst>`
  - `kb mv --doc-code <code> --to <dst>`
- 使用要求：
  - 执行前确认源和目标。
  - 不要把 `mv` 当成“修改内容”的替代品。
- 示例：

```text
kb mv --doc-code DOC_12345 --to team-workspace/archive/q1-summary.md
```

### `kb rm`

用于删除文档或空目录。只有用户明确要求时才使用。

- 常用写法：
  - `kb rm <repo/path.md>`
  - `kb rm --doc-code <code>`
- 使用要求：
  - 删除是高风险操作，必须确认目标无误。
  - 不要把“清理一下”“整理一下”自动理解成删除。
- 示例：

```text
kb rm --doc-code DOC_12345
```

### `kb publish`

用于发布文档，使内容生效。只有用户明确要求发布时才使用。

- 常用写法：
  - `kb publish --doc-code <code>`
  - `kb publish --name <repo/path.md>`
- 使用要求：
  - 不要默认发布。
  - 如果前面的写入或修改失败，先重新确认状态，再决定是否发布。
- 示例：

```text
kb publish --doc-code DOC_12345
```

## 不在本文件覆盖的内容

- 查找仓库和读取文档
- 基于 patch 的精确修改

读取类操作看 [read.md](./read.md)，精确修改看 [update.md](./update.md)。


path: references/update.md
content: # KB Patch 修改指南

从这个技能执行命令时，把下面的 `kb ...` 命令字符串整体传给 `./scripts/kb_execute.py`。如果命令本身包含多行 patch，优先把完整命令写入本地 UTF-8 文本文件，再通过 `--command-file` 发送，避免 shell 转义破坏 patch 正文。

当你需要通过 `kb edit --patch` 对现有文档做精确修改时，读这个文件。

## 工作流

1. 先把目标定位到唯一文档；一旦拿到 `code`，优先使用 `code`。
2. 用 `kb cat` 读取当前内容。
3. 只编写满足需求的最小 patch。
4. 每次 patch 只做一个文件操作。
5. 如果最终答复需要指向该文档，结束前再补 `kb render`。

## 共享规则

- 修改现有文档时默认使用 `Update File`。
- 新建文档只有在确实需要 patch 语义时才用 `Add File`；普通新建优先用 `kb write`。
- patch 中新增或替换的正文必须是 HTML，并与 `kb cat` 返回结构保持一致。
- 如果目标是 `.pdf`、`.xlsx`、`.docx` 等不可直接编辑格式，默认不要对原文件打 patch；先读取内容作参考，再创建语义等价的 `.md` 文档，除非工具明确支持原格式编辑。
- patch 不负责删除和移动。
- 能局部改就局部改，不要默认整篇重写。

## Patch 基本形状

每个 patch 必须包含：

1. `*** Begin Patch`
2. 且只包含一个文件操作头
3. patch body
4. `*** End Patch`

允许的文件操作头：

- `*** Update File --name: <repo/path.md>`
- `*** Update File --code: <doc_code>`
- `*** Add File --name: <repo/path.md>`
- `*** Add File --code: <repo_or_code_path/new.md>`

禁止的操作：

- `*** Delete File ...`
- `*** Move to ...`

## Update 规则

- 第一个 chunk 可以省略 `@@`，第二个及后续 chunk 必须显式写 `@@`。
- 上下文要尽量稳定，保证 patch 能唯一匹配。
- patch 内容不会自动 trim；需要保留空格时就原样保留。
- 如果按路径定位可能重名，先把文档 `code` 找出来再改。

最小示例：

```text
./scripts/kb_execute.py --command-file /tmp/kb-patch.txt
```

多 chunk 示例：

```text
kb edit --patch
*** Begin Patch
*** Update File --code: DOC_12345
@@ intro
-<p>旧介绍</p>
+<p>新介绍</p>

@@ footer
-<p>旧结尾</p>
+<p>新结尾</p>
*** End Patch
```

## Add 规则

- `Add File` 之后的每一行都必须以 `+` 开头。
- 空行也必须写成 `+`。
- 新增内容必须是合法的 HTML 正文。

示例：

```text
kb edit --patch
*** Begin Patch
*** Add File --name: team-workspace/docs/process/new-guideline.md
+<h1>New Guideline</h1>
+
+<p>First version.</p>
*** End Patch
```

## 失败处理

- patch 失败后，不要假设有任何 hunk 已经生效。
- 重试前先重新读取当前文档。
- 如果目标不唯一，或者文档内容已经漂移，先重新定位，不要猜。

## 不在本文件覆盖的内容

- 查找仓库和读取文档
- 普通新建、建目录、移动、删除、发布

读取类操作看 [read.md](./read.md)，普通结构操作看 [write.md](./write.md)。


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)