@comate/zulu 1.2.1-beta.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/comate-engine/assets/skills/auto-commit-comate/SKILL.md +260 -0
- package/comate-engine/assets/skills/auto-commit-comate/references/data_structures.md +189 -0
- package/comate-engine/assets/skills/auto-commit-comate/references/new_version_instruction.md +209 -0
- package/comate-engine/assets/skills/auto-commit-comate/references/old_version_instruction.md +208 -0
- package/comate-engine/assets/skills/auto-commit-comate/scripts/git_diff_cli.py +196 -0
- package/comate-engine/assets/skills/{smart-commit → auto-commit-comate}/scripts/git_utils.py +20 -10
- package/comate-engine/assets/skills/{smart-commit → auto-commit-comate}/scripts/icafe/client.py +69 -40
- package/comate-engine/assets/skills/{smart-commit → auto-commit-comate}/scripts/icafe/farseer.py +8 -9
- package/comate-engine/assets/skills/{smart-commit → auto-commit-comate}/scripts/icafe/matching.py +65 -9
- package/comate-engine/assets/skills/auto-commit-comate/scripts/match_card_cli.py +37 -0
- package/comate-engine/assets/skills/cnap-comate/SKILL.md +157 -0
- package/comate-engine/assets/skills/cnap-comate/references/cases.md +198 -0
- package/comate-engine/assets/skills/cnap-comate/references/deploy-troubleshoot.md +15 -0
- package/comate-engine/assets/skills/cnap-comate/references/install.md +43 -0
- package/comate-engine/assets/skills/cnap-comate/references/kubectl.md +55 -0
- package/comate-engine/assets/skills/cnap-comate/references/login.md +125 -0
- package/comate-engine/assets/skills/cnap-comate/references/oncall.md +24 -0
- package/comate-engine/assets/skills/cnap-comate/scripts/install_cnap_cli.sh +36 -0
- package/comate-engine/assets/skills/code-security/SKILL.md +176 -0
- package/comate-engine/assets/skills/code-security/references/credential_hosting.md +102 -0
- package/comate-engine/assets/skills/code-security/references/vul_repair_sensitive.md +219 -0
- package/comate-engine/assets/skills/code-security/scripts/build_repair_info.py +0 -0
- package/comate-engine/assets/skills/code-security/scripts/credential_hosting.py +99 -0
- package/comate-engine/assets/skills/code-security/scripts/credential_poll.py +350 -0
- package/comate-engine/assets/skills/code-security/scripts/http_client.py +173 -0
- package/comate-engine/assets/skills/code-security/scripts/parse_scan_result.py +301 -0
- package/comate-engine/assets/skills/code-security/scripts/repair_vulnerability.py +261 -0
- package/comate-engine/assets/skills/code-security/scripts/report_chat.py +198 -0
- package/comate-engine/assets/skills/code-security/scripts/scan_vulnerability.py +316 -0
- package/comate-engine/assets/skills/code-security-comate/SKILL.md +219 -0
- package/comate-engine/assets/skills/code-security-comate/references/credential_hosting.md +102 -0
- package/comate-engine/assets/skills/code-security-comate/references/vul_repair-go_sql_injection.md +399 -0
- package/comate-engine/assets/skills/code-security-comate/references/vul_repair-java_sql_injection.md +591 -0
- package/comate-engine/assets/skills/code-security-comate/references/vul_repair-php_sql_injection.md +318 -0
- package/comate-engine/assets/skills/code-security-comate/references/vul_repair-python_sql_injection.md +198 -0
- package/comate-engine/assets/skills/code-security-comate/references/vul_repair_sensitive.md +219 -0
- package/comate-engine/assets/skills/code-security-comate/scripts/credential_hosting.py +87 -0
- package/comate-engine/assets/skills/code-security-comate/scripts/credential_poll.py +345 -0
- package/comate-engine/assets/skills/code-security-comate/scripts/http_client.py +173 -0
- package/comate-engine/assets/skills/code-security-comate/scripts/parse_scan_result.py +392 -0
- package/comate-engine/assets/skills/code-security-comate/scripts/repair_vulnerability.py +245 -0
- package/comate-engine/assets/skills/code-security-comate/scripts/report_chat.py +145 -0
- package/comate-engine/assets/skills/code-security-comate/scripts/scan_vulnerability.py +444 -0
- package/comate-engine/assets/skills/code-security-comate/scripts/utils.py +153 -0
- package/comate-engine/assets/skills/comate-docs-comate/SKILL.md +148 -0
- package/comate-engine/assets/skills/comate-docs-comate/references/doc-map-extended.md +78 -0
- package/comate-engine/assets/skills/comate-docs-comate/references/models-and-billing.md +51 -0
- package/comate-engine/assets/skills/comate-docs-comate/references/product-overview.md +73 -0
- package/comate-engine/assets/skills/comate-docs-comate/references/query_content.md +83 -0
- package/comate-engine/assets/skills/comate-docs-comate/references/query_repo.md +57 -0
- package/comate-engine/assets/skills/comate-docs-comate/scripts/ku_operator.py +1575 -0
- package/comate-engine/assets/skills/create-image-comate/SKILL.md +278 -0
- package/comate-engine/assets/skills/create-skill-comate/SKILL.md +308 -217
- package/comate-engine/assets/skills/create-skill-comate/agents/analyzer.md +274 -0
- package/comate-engine/assets/skills/create-skill-comate/agents/comparator.md +202 -0
- package/comate-engine/assets/skills/create-skill-comate/agents/grader.md +223 -0
- package/comate-engine/assets/skills/create-skill-comate/assets/eval_review.html +146 -0
- package/comate-engine/assets/skills/create-skill-comate/eval-viewer/generate_review.py +489 -0
- package/comate-engine/assets/skills/create-skill-comate/eval-viewer/viewer.html +1325 -0
- package/comate-engine/assets/skills/create-skill-comate/references/schemas.md +430 -0
- package/comate-engine/assets/skills/create-skill-comate/scripts/__init__.py +0 -0
- package/comate-engine/assets/skills/create-skill-comate/scripts/__pycache__/__init__.cpython-311.pyc +0 -0
- package/comate-engine/assets/skills/create-skill-comate/scripts/__pycache__/aggregate_benchmark.cpython-311.pyc +0 -0
- package/comate-engine/assets/skills/create-skill-comate/scripts/aggregate_benchmark.py +412 -0
- package/comate-engine/assets/skills/create-skill-comate/scripts/generate_report.py +334 -0
- package/comate-engine/assets/skills/create-skill-comate/scripts/package_skill.py +140 -0
- package/comate-engine/assets/skills/create-skill-comate/scripts/utils.py +53 -0
- package/comate-engine/assets/skills/find-skills-comate/SKILL.md +15 -12
- package/comate-engine/assets/skills/find-skills-comate/scripts/fetch_skills.py +32 -3
- package/comate-engine/assets/skills/get-ugate-token-comate/SKILL.md +159 -0
- package/comate-engine/assets/skills/get-ugate-token-comate/getUgateToken.py +150 -0
- package/comate-engine/assets/skills/icafe-comate/SKILL.md +240 -0
- package/comate-engine/assets/skills/icafe-comate/references/ai-workflows.md +233 -0
- package/comate-engine/assets/skills/icafe-comate/references/commands.md +1147 -0
- package/comate-engine/assets/skills/icafe-comate/references/error-handling.md +164 -0
- package/comate-engine/assets/skills/icafe-comate/references/git-auto-bindcard-workflow.md +201 -0
- package/comate-engine/assets/skills/icafe-comate/references/git-bindcard-workflow.md +327 -0
- package/comate-engine/assets/skills/icafe-comate/references/iql-syntax.md +327 -0
- package/comate-engine/assets/skills/icafe-comate/references/platform-concepts.md +317 -0
- package/comate-engine/assets/skills/icafe-comate/references/smart-create-workflow.md +171 -0
- package/comate-engine/assets/skills/icafe-comate/references/smart-find-workflow.md +127 -0
- package/comate-engine/assets/skills/icafe-comate/references/smart-update-workflow.md +118 -0
- package/comate-engine/assets/skills/icode-comate/SKILL.md +366 -0
- package/comate-engine/assets/skills/icode-comate/references/api/add_reviewers.md +44 -0
- package/comate-engine/assets/skills/icode-comate/references/api/build_fetch_command.md +89 -0
- package/comate-engine/assets/skills/icode-comate/references/api/check_repo_permission.md +89 -0
- package/comate-engine/assets/skills/icode-comate/references/api/create_branch.md +79 -0
- package/comate-engine/assets/skills/icode-comate/references/api/create_draft_comment.md +109 -0
- package/comate-engine/assets/skills/icode-comate/references/api/get_ai_cr_result.md +190 -0
- package/comate-engine/assets/skills/icode-comate/references/api/get_ai_review.md +97 -0
- package/comate-engine/assets/skills/icode-comate/references/api/get_diff_content.md +92 -0
- package/comate-engine/assets/skills/icode-comate/references/api/get_diff_file.md +88 -0
- package/comate-engine/assets/skills/icode-comate/references/api/get_machine_check.md +73 -0
- package/comate-engine/assets/skills/icode-comate/references/api/get_my_reviews.md +115 -0
- package/comate-engine/assets/skills/icode-comate/references/api/get_person_commit.md +89 -0
- package/comate-engine/assets/skills/icode-comate/references/api/get_person_repo.md +63 -0
- package/comate-engine/assets/skills/icode-comate/references/api/get_repo_branch.md +62 -0
- package/comate-engine/assets/skills/icode-comate/references/api/get_repo_config.md +91 -0
- package/comate-engine/assets/skills/icode-comate/references/api/get_repo_members.md +118 -0
- package/comate-engine/assets/skills/icode-comate/references/api/get_repo_reviews.md +91 -0
- package/comate-engine/assets/skills/icode-comate/references/api/get_review_comments.md +87 -0
- package/comate-engine/assets/skills/icode-comate/references/api/get_review_info.md +81 -0
- package/comate-engine/assets/skills/icode-comate/references/api/get_submit_settings.md +105 -0
- package/comate-engine/assets/skills/icode-comate/references/api/icode-api.md +86 -0
- package/comate-engine/assets/skills/icode-comate/references/api/publish_comments.md +72 -0
- package/comate-engine/assets/skills/icode-comate/references/api/set_review_score.md +58 -0
- package/comate-engine/assets/skills/icode-comate/references/api/start_ai_review.md +77 -0
- package/comate-engine/assets/skills/icode-comate/references/api/submit_review.md +50 -0
- package/comate-engine/assets/skills/icode-comate/references/api/trigger_ai_cr.md +63 -0
- package/comate-engine/assets/skills/icode-comate/references/feature/add-reviewer.md +92 -0
- package/comate-engine/assets/skills/icode-comate/references/feature/fix-machine-check.md +144 -0
- package/comate-engine/assets/skills/icode-comate/references/feature/merge-cr.md +100 -0
- package/comate-engine/assets/skills/icode-comate/references/feature/ssh-setup.md +106 -0
- package/comate-engine/assets/skills/icode-comate/references/feature/submit-acr.md +135 -0
- package/comate-engine/assets/skills/icode-comate/references/feature/submit-cr.md +123 -0
- package/comate-engine/assets/skills/icode-comate/references/git/clone.md +67 -0
- package/comate-engine/assets/skills/icode-comate/references/git/icode-git.md +68 -0
- package/comate-engine/assets/skills/icode-comate/references/git/push.md +64 -0
- package/comate-engine/assets/skills/icode-comate/references/git/push_cr.md +103 -0
- package/comate-engine/assets/skills/icode-comate/references/install.md +144 -0
- package/comate-engine/assets/skills/icode-comate/references/login.md +111 -0
- package/comate-engine/assets/skills/icode-comate/scripts/add-reviewer.sh +154 -0
- package/comate-engine/assets/skills/icode-comate/scripts/common.sh +145 -0
- package/comate-engine/assets/skills/icode-comate/scripts/fix-machine-check.sh +131 -0
- package/comate-engine/assets/skills/icode-comate/scripts/merge-cr.sh +105 -0
- package/comate-engine/assets/skills/icode-comate/scripts/ssh-setup.sh +159 -0
- package/comate-engine/assets/skills/icode-comate/scripts/submit-acr.sh +236 -0
- package/comate-engine/assets/skills/icode-comate/scripts/submit-cr.sh +104 -0
- package/comate-engine/assets/skills/icode-comate/scripts/test-preflight.sh +89 -0
- package/comate-engine/assets/skills/ku-operator-comate/SKILL.md +121 -0
- package/comate-engine/assets/skills/ku-operator-comate/examples.md +190 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/add_member.md +49 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/change_scope.md +38 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/copy_doc.md +50 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/create_doc.md +61 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/delete_doc.md +31 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/edit_content.md +568 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/move_doc.md +45 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/query_comment.md +79 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/query_content.md +83 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/query_flowchart.md +84 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/query_permission.md +38 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/query_recent_view.md +67 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/query_repo.md +57 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/query_user_info.md +37 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/update_member.md +41 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/upload_attachment.md +52 -0
- package/comate-engine/assets/skills/ku-operator-comate/scripts/ku_operator.py +1575 -0
- package/comate-engine/node_modules/better-sqlite3/node_modules/.bin/prebuild-install +2 -2
- package/comate-engine/node_modules/tree-sitter-bash/node_modules/.bin/node-gyp-build +2 -2
- package/comate-engine/node_modules/tree-sitter-bash/node_modules/.bin/node-gyp-build-optional +2 -2
- package/comate-engine/node_modules/tree-sitter-bash/node_modules/.bin/node-gyp-build-test +2 -2
- package/comate-engine/package.json +2 -0
- package/comate-engine/server.js +263 -79
- package/dist/bundle/index.js +3 -3
- package/package.json +1 -1
- package/comate-engine/assets/skills/figma2code-comate/codeConnect.md +0 -37
- package/comate-engine/assets/skills/figma2code-comate/designToken.md +0 -3
- package/comate-engine/assets/skills/figma2code-comate/f2cMcp.md +0 -59
- package/comate-engine/assets/skills/smart-commit/SKILL.md +0 -646
- package/comate-engine/node_modules/@comate/plugin-host/dist/index-AZIho4HV.js +0 -1
- package/comate-engine/node_modules/@comate/plugin-host/dist/user-BIpzRUfb.js +0 -44
- /package/comate-engine/assets/skills/{smart-commit → auto-commit-comate}/references/issue_type_mapping.json +0 -0
- /package/comate-engine/assets/skills/{smart-commit → auto-commit-comate}/references/query_reference.md +0 -0
- /package/comate-engine/assets/skills/{smart-commit → auto-commit-comate}/scripts/compat.py +0 -0
- /package/comate-engine/assets/skills/{smart-commit → auto-commit-comate}/scripts/create_card_cli.py +0 -0
- /package/comate-engine/assets/skills/{smart-commit → auto-commit-comate}/scripts/icafe/__init__.py +0 -0
- /package/comate-engine/assets/skills/{smart-commit → auto-commit-comate}/scripts/logger.py +0 -0
- /package/comate-engine/assets/skills/{smart-commit → auto-commit-comate}/scripts/recognize_card_cli.py +0 -0
|
@@ -0,0 +1,1575 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
知识库(Ku)文档操作 CLI 工具
|
|
5
|
+
|
|
6
|
+
支持的操作:
|
|
7
|
+
1. query-content - 查询文档内容
|
|
8
|
+
2. query-repo - 查询知识库目录
|
|
9
|
+
3. create-doc - 新建文档/目录
|
|
10
|
+
4. move-doc - 移动文档
|
|
11
|
+
5. delete-doc - 删除文档
|
|
12
|
+
6. query-user-info - 查询当前用户信息
|
|
13
|
+
7. copy-doc - 复制文档
|
|
14
|
+
8. edit-content - 编辑文档正文
|
|
15
|
+
9. upload-attachment - 上传附件到文档正文
|
|
16
|
+
10. query-flowchart - 查询文档流程图数据
|
|
17
|
+
11. add-member - 添加文档成员
|
|
18
|
+
12. update-member - 更新文档成员角色
|
|
19
|
+
13. change-scope - 修改文档公开/私密范围
|
|
20
|
+
14. query-permission - 查询用户文档权限
|
|
21
|
+
15. query-comment - 查询文档评论
|
|
22
|
+
16. query-recent-view - 查询文档浏览记录
|
|
23
|
+
|
|
24
|
+
依赖:pip install requests
|
|
25
|
+
|
|
26
|
+
认证方式:通过 ugate-auth skill 获取个人身份 token
|
|
27
|
+
环境变量:
|
|
28
|
+
- COMATE_USERNAME: 当前用户的百度用户名(必填)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import argparse
|
|
32
|
+
import json
|
|
33
|
+
import os
|
|
34
|
+
import re
|
|
35
|
+
import subprocess
|
|
36
|
+
import sys
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
39
|
+
from urllib.parse import urlparse, unquote
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
import requests
|
|
44
|
+
except ImportError:
|
|
45
|
+
print("错误: 缺少 requests 库,请执行 pip install requests", file=sys.stderr)
|
|
46
|
+
sys.exit(1)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# 常量
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
DEFAULT_API_BASE = "https://apigo.baidu-int.com/wiki/so"
|
|
54
|
+
JSON_TO_MD_URL = "http://10.11.153.100:8001/jsonToMd"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
# API 客户端
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
class KuApiClient:
|
|
62
|
+
"""知识库 REST API 客户端"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, base_url: str, ugate_token: str):
|
|
65
|
+
"""初始化知识库 API 客户端。"""
|
|
66
|
+
self.base_url = base_url.rstrip("/")
|
|
67
|
+
self._session = requests.Session()
|
|
68
|
+
self._session.headers["Ugate-Token"] = ugate_token
|
|
69
|
+
self._session.headers["access-key"] = "mYUuWUsFKk7e/oAjCKC8CA=="
|
|
70
|
+
self._session.headers["Content-Type"] = "application/json"
|
|
71
|
+
|
|
72
|
+
def _post(self, path: str, payload: Dict[str, Any], timeout: int = 30) -> Dict[str, Any]:
|
|
73
|
+
"""发送 JSON POST 请求并返回响应。"""
|
|
74
|
+
url = f"{self.base_url}{path}"
|
|
75
|
+
try:
|
|
76
|
+
resp = self._session.post(url, json=payload, timeout=timeout)
|
|
77
|
+
except requests.exceptions.Timeout:
|
|
78
|
+
print(f"错误: 请求超时 ({timeout}s),URL: {path}", file=sys.stderr)
|
|
79
|
+
print("建议: 可能是服务端处理耗时较长,请稍后重试。", file=sys.stderr)
|
|
80
|
+
sys.exit(1)
|
|
81
|
+
if resp.status_code in (401, 403):
|
|
82
|
+
msg = f"错误: 认证失败 (HTTP {resp.status_code})," \
|
|
83
|
+
"ugate token 可能已过期或无效。"
|
|
84
|
+
print(msg, file=sys.stderr)
|
|
85
|
+
print("请重新发起当前操作,系统将自动触发 ugate-auth 授权流程。",
|
|
86
|
+
file=sys.stderr)
|
|
87
|
+
sys.exit(2)
|
|
88
|
+
if resp.status_code == 504:
|
|
89
|
+
print(f"错误: 网关超时 (HTTP 504),URL: {path}", file=sys.stderr)
|
|
90
|
+
print("建议: 服务端处理超时,请稍后重试。如果操作涉及大量内容,"
|
|
91
|
+
"可尝试拆分为多次小批量操作。", file=sys.stderr)
|
|
92
|
+
sys.exit(1)
|
|
93
|
+
if resp.status_code >= 500:
|
|
94
|
+
print(f"错误: 服务端错误 (HTTP {resp.status_code}),URL: {path}",
|
|
95
|
+
file=sys.stderr)
|
|
96
|
+
print("建议: 服务端暂时不可用,请稍后重试。", file=sys.stderr)
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
resp.raise_for_status()
|
|
99
|
+
return resp.json()
|
|
100
|
+
|
|
101
|
+
def _post_multipart(
|
|
102
|
+
self, path: str, form_data: Dict[str, Any],
|
|
103
|
+
files: Dict[str, Any], timeout: int = 120,
|
|
104
|
+
) -> Dict[str, Any]:
|
|
105
|
+
"""发送 multipart/form-data POST 请求并返回响应。"""
|
|
106
|
+
url = f"{self.base_url}{path}"
|
|
107
|
+
headers = dict(self._session.headers)
|
|
108
|
+
if 'Content-Type' in headers:
|
|
109
|
+
del headers['Content-Type']
|
|
110
|
+
try:
|
|
111
|
+
resp = requests.post(url, headers=headers, data=form_data, files=files, timeout=timeout)
|
|
112
|
+
except requests.exceptions.Timeout:
|
|
113
|
+
print(f"错误: 请求超时 ({timeout}s),URL: {path}", file=sys.stderr)
|
|
114
|
+
print("建议: 文件上传超时,请检查文件大小或稍后重试。", file=sys.stderr)
|
|
115
|
+
sys.exit(1)
|
|
116
|
+
if resp.status_code in (401, 403):
|
|
117
|
+
msg = f"错误: 认证失败 (HTTP {resp.status_code})," \
|
|
118
|
+
"ugate token 可能已过期或无效。"
|
|
119
|
+
print(msg, file=sys.stderr)
|
|
120
|
+
print("请重新发起当前操作,系统将自动触发 ugate-auth 授权流程。",
|
|
121
|
+
file=sys.stderr)
|
|
122
|
+
sys.exit(2)
|
|
123
|
+
if resp.status_code >= 500:
|
|
124
|
+
print(f"错误: 服务端错误 (HTTP {resp.status_code}),URL: {path}",
|
|
125
|
+
file=sys.stderr)
|
|
126
|
+
print("建议: 服务端暂时不可用,请稍后重试。", file=sys.stderr)
|
|
127
|
+
sys.exit(1)
|
|
128
|
+
resp.raise_for_status()
|
|
129
|
+
return resp.json()
|
|
130
|
+
|
|
131
|
+
# ----- 1. 查询文档内容 -----
|
|
132
|
+
|
|
133
|
+
def query_content(
|
|
134
|
+
self,
|
|
135
|
+
doc_id: Optional[str] = None,
|
|
136
|
+
url: Optional[str] = None,
|
|
137
|
+
) -> Dict[str, Any]:
|
|
138
|
+
"""
|
|
139
|
+
查询文档内容。
|
|
140
|
+
:param doc_id: 文档 ID(与 url 二选一)
|
|
141
|
+
:param url: 文档 URL(与 doc_id 二选一)
|
|
142
|
+
"""
|
|
143
|
+
if not doc_id and not url:
|
|
144
|
+
raise ValueError("必须提供 --doc-id 或 --url 中的至少一个")
|
|
145
|
+
payload: Dict[str, Any] = {}
|
|
146
|
+
if doc_id:
|
|
147
|
+
payload["docId"] = doc_id
|
|
148
|
+
if url:
|
|
149
|
+
payload["url"] = url
|
|
150
|
+
return self._post("/ku/openapi/queryContent", payload)
|
|
151
|
+
|
|
152
|
+
# ----- 2. 查询知识库目录 -----
|
|
153
|
+
|
|
154
|
+
REPO_PAGE_SIZE = 100 # 固定每页数量,不对外暴露
|
|
155
|
+
ROOT_DOC_GUID = "00000000000000" # 根目录标识
|
|
156
|
+
|
|
157
|
+
def query_repo(
|
|
158
|
+
self,
|
|
159
|
+
repo_guid: str,
|
|
160
|
+
parent_doc_guid: str = "00000000000000",
|
|
161
|
+
page_num: int = 1,
|
|
162
|
+
) -> Dict[str, Any]:
|
|
163
|
+
"""
|
|
164
|
+
查询知识库目录列表。
|
|
165
|
+
:param repo_guid: 知识库 ID(必填)
|
|
166
|
+
:param parent_doc_guid: 父文档 ID(默认 00000000000000 即根目录)
|
|
167
|
+
:param page_num: 页码,默认 1
|
|
168
|
+
"""
|
|
169
|
+
payload: Dict[str, Any] = {
|
|
170
|
+
"repoId": repo_guid,
|
|
171
|
+
"parentDocGuid": parent_doc_guid,
|
|
172
|
+
"pageNum": page_num,
|
|
173
|
+
"pageSize": self.REPO_PAGE_SIZE,
|
|
174
|
+
}
|
|
175
|
+
return self._post("/ku/openapi/queryRepo", payload)
|
|
176
|
+
|
|
177
|
+
# ----- 3. 新建文档/目录 -----
|
|
178
|
+
|
|
179
|
+
def create_doc(
|
|
180
|
+
self,
|
|
181
|
+
repository_guid: str,
|
|
182
|
+
title: Optional[str] = None,
|
|
183
|
+
content: Optional[str] = None,
|
|
184
|
+
parent_doc_guid: Optional[str] = None,
|
|
185
|
+
) -> Dict[str, Any]:
|
|
186
|
+
"""
|
|
187
|
+
新建文档或目录。
|
|
188
|
+
:param repository_guid: 知识库 ID(必填)
|
|
189
|
+
:param title: 文档标题
|
|
190
|
+
:param content: 文档内容(Markdown 格式)
|
|
191
|
+
:param parent_doc_guid: 父文档 ID(不传则创建在根目录)
|
|
192
|
+
"""
|
|
193
|
+
creator_username = os.environ.get("COMATE_USERNAME", "")
|
|
194
|
+
if not creator_username:
|
|
195
|
+
print("警告: 未设置环境变量 COMATE_USERNAME,将使用空用户名", file=sys.stderr)
|
|
196
|
+
payload: Dict[str, Any] = {
|
|
197
|
+
"repositoryGuid": repository_guid,
|
|
198
|
+
"creatorUsername": creator_username,
|
|
199
|
+
}
|
|
200
|
+
if title:
|
|
201
|
+
payload["title"] = title
|
|
202
|
+
if content:
|
|
203
|
+
payload["content"] = content
|
|
204
|
+
if parent_doc_guid:
|
|
205
|
+
payload["parentDocGuid"] = parent_doc_guid
|
|
206
|
+
return self._post("/ku/openapi/createDoc", payload)
|
|
207
|
+
|
|
208
|
+
# ----- 4. 移动文档 -----
|
|
209
|
+
|
|
210
|
+
def move_doc(
|
|
211
|
+
self,
|
|
212
|
+
doc_id: str,
|
|
213
|
+
to_repo_guid: str,
|
|
214
|
+
to_parent_guid: Optional[str] = None,
|
|
215
|
+
) -> Dict[str, Any]:
|
|
216
|
+
"""
|
|
217
|
+
移动文档到目标位置。
|
|
218
|
+
:param doc_id: 文档 ID(必填)
|
|
219
|
+
:param to_repo_guid: 目标知识库 ID(必填)
|
|
220
|
+
:param to_parent_guid: 目标父目录 ID(不传则移动到根目录)
|
|
221
|
+
"""
|
|
222
|
+
operator_username = os.environ.get("COMATE_USERNAME", "")
|
|
223
|
+
if not operator_username:
|
|
224
|
+
print("警告: 未设置环境变量 COMATE_USERNAME,将使用空用户名", file=sys.stderr)
|
|
225
|
+
payload: Dict[str, Any] = {
|
|
226
|
+
"docId": doc_id,
|
|
227
|
+
"toRepoGuid": to_repo_guid,
|
|
228
|
+
"operatorUsername": operator_username,
|
|
229
|
+
}
|
|
230
|
+
if to_parent_guid:
|
|
231
|
+
payload["toParentGuid"] = to_parent_guid
|
|
232
|
+
return self._post("/ku/openapi/moveDoc", payload)
|
|
233
|
+
|
|
234
|
+
# ----- 5. 删除文档 -----
|
|
235
|
+
|
|
236
|
+
def delete_doc(self, doc_id: str) -> Dict[str, Any]:
|
|
237
|
+
"""
|
|
238
|
+
删除文档。
|
|
239
|
+
:param doc_id: 文档 ID(必填)
|
|
240
|
+
"""
|
|
241
|
+
operator_username = os.environ.get("COMATE_USERNAME", "")
|
|
242
|
+
if not operator_username:
|
|
243
|
+
print("警告: 未设置环境变量 COMATE_USERNAME,将使用空用户名", file=sys.stderr)
|
|
244
|
+
payload: Dict[str, Any] = {
|
|
245
|
+
"docId": doc_id,
|
|
246
|
+
"operatorUsername": operator_username,
|
|
247
|
+
}
|
|
248
|
+
return self._post("/ku/openapi/deleteDoc", payload)
|
|
249
|
+
|
|
250
|
+
# ----- 6. 查询用户信息 -----
|
|
251
|
+
|
|
252
|
+
def query_user_info(self, username: str) -> Dict[str, Any]:
|
|
253
|
+
"""
|
|
254
|
+
查询用户信息。
|
|
255
|
+
:param username: 用户名(必填)
|
|
256
|
+
"""
|
|
257
|
+
return self._post("/ku/openapi/queryUserInfo", {"username": username})
|
|
258
|
+
|
|
259
|
+
# ----- 7. 复制文档 -----
|
|
260
|
+
|
|
261
|
+
def copy_doc(
|
|
262
|
+
self,
|
|
263
|
+
doc_id: str,
|
|
264
|
+
to_repo_guid: Optional[str] = None,
|
|
265
|
+
to_parent_guid: Optional[str] = None,
|
|
266
|
+
new_title: Optional[str] = None,
|
|
267
|
+
) -> Dict[str, Any]:
|
|
268
|
+
"""复制文档到目标位置。"""
|
|
269
|
+
operator_username = os.environ.get("COMATE_USERNAME", "")
|
|
270
|
+
payload: Dict[str, Any] = {
|
|
271
|
+
"docId": doc_id,
|
|
272
|
+
"operatorUsername": operator_username,
|
|
273
|
+
}
|
|
274
|
+
if to_repo_guid:
|
|
275
|
+
payload["toRepoGuid"] = to_repo_guid
|
|
276
|
+
if to_parent_guid:
|
|
277
|
+
payload["toParentGuid"] = to_parent_guid
|
|
278
|
+
if new_title:
|
|
279
|
+
payload["newTitle"] = new_title
|
|
280
|
+
return self._post("/ku/openapi/copyDoc", payload)
|
|
281
|
+
|
|
282
|
+
# ----- 8. 编辑文档正文 -----
|
|
283
|
+
|
|
284
|
+
def edit_content(
|
|
285
|
+
self,
|
|
286
|
+
doc_id: str,
|
|
287
|
+
operations: List[Dict[str, Any]],
|
|
288
|
+
publish: bool = False,
|
|
289
|
+
) -> Dict[str, Any]:
|
|
290
|
+
"""编辑文档正文内容。"""
|
|
291
|
+
username = os.environ.get("COMATE_USERNAME", "")
|
|
292
|
+
payload: Dict[str, Any] = {
|
|
293
|
+
"docGuid": doc_id,
|
|
294
|
+
"editorUsername": username,
|
|
295
|
+
"operations": operations,
|
|
296
|
+
}
|
|
297
|
+
if publish:
|
|
298
|
+
payload["publish"] = True
|
|
299
|
+
return self._post("/ku/openapi/editContent", payload)
|
|
300
|
+
|
|
301
|
+
# ----- 9. 上传附件 -----
|
|
302
|
+
|
|
303
|
+
def upload_attachment(self, doc_id: str, file_path: str) -> Dict[str, Any]:
|
|
304
|
+
"""上传附件到指定文档。"""
|
|
305
|
+
with open(file_path, 'rb') as f:
|
|
306
|
+
file_content = f.read()
|
|
307
|
+
file_name = os.path.basename(file_path)
|
|
308
|
+
form_data = {"docGuid": doc_id}
|
|
309
|
+
files = {"file": (file_name, file_content)}
|
|
310
|
+
return self._post_multipart("/ku/openapi/uploadAttachment", form_data, files)
|
|
311
|
+
|
|
312
|
+
# ----- 10. 查询流程图 -----
|
|
313
|
+
|
|
314
|
+
def query_flowchart(self, doc_id: str, flowchart_id: str) -> Dict[str, Any]:
|
|
315
|
+
"""查询文档中的流程图数据。"""
|
|
316
|
+
return self._post("/ku/openapi/queryFlowchart", {
|
|
317
|
+
"docGuid": doc_id,
|
|
318
|
+
"flowchartId": flowchart_id,
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
# ----- 14. 添加成员 -----
|
|
322
|
+
|
|
323
|
+
def add_member(
|
|
324
|
+
self,
|
|
325
|
+
doc_id: str,
|
|
326
|
+
usernames: List[str],
|
|
327
|
+
role: str = "DocReader",
|
|
328
|
+
) -> Dict[str, Any]:
|
|
329
|
+
"""添加文档成员。"""
|
|
330
|
+
return self._post("/ku/openapi/addMember", {
|
|
331
|
+
"docId": doc_id,
|
|
332
|
+
"usernames": usernames,
|
|
333
|
+
"roleName": role,
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
# ----- 15. 更新成员角色 -----
|
|
337
|
+
|
|
338
|
+
def update_member(
|
|
339
|
+
self,
|
|
340
|
+
doc_id: str,
|
|
341
|
+
username: str,
|
|
342
|
+
role: str,
|
|
343
|
+
) -> Dict[str, Any]:
|
|
344
|
+
"""更新文档成员角色。"""
|
|
345
|
+
return self._post("/ku/openapi/updateMember", {
|
|
346
|
+
"docId": doc_id,
|
|
347
|
+
"username": username,
|
|
348
|
+
"roleName": role,
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
# ----- 16. 修改文档范围 -----
|
|
352
|
+
|
|
353
|
+
def change_scope(self, doc_id: str, scope: int) -> Dict[str, Any]:
|
|
354
|
+
"""修改文档公开/私密范围。"""
|
|
355
|
+
operator_username = os.environ.get("COMATE_USERNAME", "")
|
|
356
|
+
return self._post("/ku/openapi/changeScope", {
|
|
357
|
+
"docId": doc_id,
|
|
358
|
+
"scope": scope,
|
|
359
|
+
"operatorUsername": operator_username,
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
# ----- 17. 查询权限 -----
|
|
363
|
+
|
|
364
|
+
def query_permission(
|
|
365
|
+
self,
|
|
366
|
+
doc_id: str,
|
|
367
|
+
usernames: List[str],
|
|
368
|
+
) -> Dict[str, Any]:
|
|
369
|
+
"""查询用户对文档的权限。"""
|
|
370
|
+
return self._post("/ku/openapi/queryPermission", {
|
|
371
|
+
"docId": doc_id,
|
|
372
|
+
"usernames": usernames,
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
# ----- 18. 查询评论 -----
|
|
376
|
+
|
|
377
|
+
def query_comment(
|
|
378
|
+
self,
|
|
379
|
+
doc_id: str,
|
|
380
|
+
bottom: bool = True,
|
|
381
|
+
side: bool = False,
|
|
382
|
+
page: int = 1,
|
|
383
|
+
page_size: int = 100,
|
|
384
|
+
) -> Dict[str, Any]:
|
|
385
|
+
"""查询文档评论。"""
|
|
386
|
+
payload: Dict[str, Any] = {
|
|
387
|
+
"docId": doc_id,
|
|
388
|
+
"queryBottomComment": bottom,
|
|
389
|
+
"querySideComment": side,
|
|
390
|
+
"pageNum": page,
|
|
391
|
+
"pageSize": page_size,
|
|
392
|
+
}
|
|
393
|
+
return self._post("/ku/openapi/queryComments", payload)
|
|
394
|
+
|
|
395
|
+
# ----- 19. 查询浏览记录 -----
|
|
396
|
+
|
|
397
|
+
def query_recent_view(
|
|
398
|
+
self,
|
|
399
|
+
doc_id: str,
|
|
400
|
+
begin_time: Optional[int] = None,
|
|
401
|
+
end_time: Optional[int] = None,
|
|
402
|
+
page: int = 1,
|
|
403
|
+
page_size: int = 100,
|
|
404
|
+
) -> Dict[str, Any]:
|
|
405
|
+
"""查询文档浏览记录。"""
|
|
406
|
+
payload: Dict[str, Any] = {
|
|
407
|
+
"docId": doc_id,
|
|
408
|
+
"pageNum": page,
|
|
409
|
+
"pageSize": page_size,
|
|
410
|
+
}
|
|
411
|
+
if begin_time is not None:
|
|
412
|
+
payload["beginTime"] = begin_time
|
|
413
|
+
if end_time is not None:
|
|
414
|
+
payload["endTime"] = end_time
|
|
415
|
+
return self._post("/ku/openapi/queryRecentView", payload)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
# ---------------------------------------------------------------------------
|
|
419
|
+
# 内容转换工具
|
|
420
|
+
# ---------------------------------------------------------------------------
|
|
421
|
+
|
|
422
|
+
def convert_blocks_to_markdown(blocks: List[Dict[str, Any]]) -> str:
|
|
423
|
+
"""调用 jsonToMd 服务将知识库文档的 block 结构转换为 Markdown 格式。"""
|
|
424
|
+
try:
|
|
425
|
+
resp = requests.post(
|
|
426
|
+
JSON_TO_MD_URL,
|
|
427
|
+
json={"data": blocks},
|
|
428
|
+
headers={"Content-Type": "application/json"},
|
|
429
|
+
timeout=60,
|
|
430
|
+
)
|
|
431
|
+
resp.raise_for_status()
|
|
432
|
+
result = resp.json()
|
|
433
|
+
if result.get("code") == 200:
|
|
434
|
+
return result.get("data", "")
|
|
435
|
+
print(f"警告: jsonToMd 服务返回异常: {result}", file=sys.stderr)
|
|
436
|
+
return ""
|
|
437
|
+
except Exception as e:
|
|
438
|
+
print(f"错误: 调用 jsonToMd 服务失败: {e}", file=sys.stderr)
|
|
439
|
+
sys.exit(1)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
# ---------------------------------------------------------------------------
|
|
443
|
+
# 资源处理工具(图片/附件下载与上传)
|
|
444
|
+
# ---------------------------------------------------------------------------
|
|
445
|
+
|
|
446
|
+
# Markdown 图片引用正则:
|
|
447
|
+
_MD_IMAGE_PATTERN = re.compile(r'(!\[[^\]]*\])\(([^)]+)\)')
|
|
448
|
+
|
|
449
|
+
# Markdown 链接引用正则:[text](url)
|
|
450
|
+
_MD_LINK_PATTERN = re.compile(r'(?<!!)(\[[^\]]*\])\(([^)]+)\)')
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _format_file_size(size_bytes: int) -> str:
|
|
454
|
+
"""格式化文件大小为人类可读格式。"""
|
|
455
|
+
if size_bytes < 1024:
|
|
456
|
+
return f"{size_bytes}B"
|
|
457
|
+
elif size_bytes < 1024 * 1024:
|
|
458
|
+
return f"{size_bytes / 1024:.1f}KB"
|
|
459
|
+
else:
|
|
460
|
+
return f"{size_bytes / (1024 * 1024):.1f}MB"
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
# MIME 类型 → 文件扩展名映射
|
|
464
|
+
_MIME_TO_EXT: Dict[str, str] = {
|
|
465
|
+
"image/png": ".png",
|
|
466
|
+
"image/jpeg": ".jpg",
|
|
467
|
+
"image/jpg": ".jpg",
|
|
468
|
+
"image/gif": ".gif",
|
|
469
|
+
"image/webp": ".webp",
|
|
470
|
+
"image/svg+xml": ".svg",
|
|
471
|
+
"image/bmp": ".bmp",
|
|
472
|
+
"image/tiff": ".tiff",
|
|
473
|
+
"image/x-icon": ".ico",
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _ext_from_mime(mime_type: str) -> str:
|
|
478
|
+
"""根据 MIME 类型返回文件扩展名,未知类型返回 '.bin'。"""
|
|
479
|
+
if not mime_type:
|
|
480
|
+
return ".bin"
|
|
481
|
+
return _MIME_TO_EXT.get(mime_type.lower().split(";")[0].strip(), ".bin")
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def download_assets_from_blocks(
|
|
485
|
+
markdown: str,
|
|
486
|
+
blocks: List[Dict[str, Any]],
|
|
487
|
+
doc_id: str,
|
|
488
|
+
save_dir: str = "./ku_assets",
|
|
489
|
+
) -> Tuple[str, List[Dict[str, Any]]]:
|
|
490
|
+
"""
|
|
491
|
+
从原始 content blocks 中提取图片和附件信息,下载图片到本地,
|
|
492
|
+
并在 markdown 中替换图片 URL 为本地路径、替换附件占位符为详细元信息。
|
|
493
|
+
|
|
494
|
+
jsonToMd 服务将 type=image 转为 、type=attachment 转为 [附件] 占位符,
|
|
495
|
+
会丢失附件的文件名/大小/类型等关键信息。本函数直接从原始 blocks 提取完整元数据。
|
|
496
|
+
|
|
497
|
+
:param markdown: jsonToMd 转换后的 markdown 文本
|
|
498
|
+
:param blocks: queryContent API 返回的原始 content block 列表
|
|
499
|
+
:param doc_id: 文档 ID,用于创建子目录
|
|
500
|
+
:param save_dir: 资源保存根目录
|
|
501
|
+
:return: (修改后的 markdown, 下载/识别的资源清单列表)
|
|
502
|
+
"""
|
|
503
|
+
# 从 blocks 中提取图片和附件
|
|
504
|
+
images: List[Dict[str, Any]] = []
|
|
505
|
+
attachments: List[Dict[str, Any]] = []
|
|
506
|
+
for block in blocks:
|
|
507
|
+
btype = block.get("type", "")
|
|
508
|
+
if btype == "image":
|
|
509
|
+
src = block.get("src", "")
|
|
510
|
+
if src:
|
|
511
|
+
images.append({
|
|
512
|
+
"url": src,
|
|
513
|
+
"mimeType": block.get("mimeType", ""),
|
|
514
|
+
})
|
|
515
|
+
elif btype == "attachment":
|
|
516
|
+
file_info = block.get("fileInfo") or {}
|
|
517
|
+
attachments.append({
|
|
518
|
+
"fileId": block.get("fileId", ""),
|
|
519
|
+
"docId": block.get("docId", doc_id),
|
|
520
|
+
"name": file_info.get("name", "未知文件"),
|
|
521
|
+
"extension": file_info.get("extension", ""),
|
|
522
|
+
"size": file_info.get("size", 0),
|
|
523
|
+
"mimeType": file_info.get("type", ""),
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
if not images and not attachments:
|
|
527
|
+
return markdown, []
|
|
528
|
+
|
|
529
|
+
# 下载图片
|
|
530
|
+
downloaded: List[Dict[str, Any]] = []
|
|
531
|
+
failed: List[Dict[str, str]] = []
|
|
532
|
+
|
|
533
|
+
if images:
|
|
534
|
+
asset_dir = os.path.join(save_dir, doc_id)
|
|
535
|
+
os.makedirs(asset_dir, exist_ok=True)
|
|
536
|
+
|
|
537
|
+
for i, img in enumerate(images):
|
|
538
|
+
ext = _ext_from_mime(img["mimeType"])
|
|
539
|
+
filename = f"image_{i + 1:03d}{ext}"
|
|
540
|
+
local_path = os.path.join(asset_dir, filename)
|
|
541
|
+
try:
|
|
542
|
+
resp = requests.get(img["url"], timeout=30)
|
|
543
|
+
resp.raise_for_status()
|
|
544
|
+
# 如果 block 没有 mimeType,从 Content-Type 推断
|
|
545
|
+
if ext == ".bin":
|
|
546
|
+
ct = resp.headers.get("Content-Type", "")
|
|
547
|
+
header_ext = _ext_from_mime(ct)
|
|
548
|
+
if header_ext != ".bin":
|
|
549
|
+
ext = header_ext
|
|
550
|
+
filename = f"image_{i + 1:03d}{ext}"
|
|
551
|
+
local_path = os.path.join(asset_dir, filename)
|
|
552
|
+
with open(local_path, 'wb') as f:
|
|
553
|
+
f.write(resp.content)
|
|
554
|
+
downloaded.append({
|
|
555
|
+
'type': 'image',
|
|
556
|
+
'url': img['url'],
|
|
557
|
+
'local_path': local_path,
|
|
558
|
+
'filename': filename,
|
|
559
|
+
'size': len(resp.content),
|
|
560
|
+
'mimeType': img['mimeType'] or ct if ext != ".bin" else "",
|
|
561
|
+
})
|
|
562
|
+
except Exception as e:
|
|
563
|
+
failed.append({'type': 'image', 'url': img['url'], 'error': str(e)})
|
|
564
|
+
|
|
565
|
+
# 替换 markdown 中的图片 URL 为本地路径
|
|
566
|
+
modified_markdown = markdown
|
|
567
|
+
for item in downloaded:
|
|
568
|
+
modified_markdown = modified_markdown.replace(item['url'], item['local_path'])
|
|
569
|
+
|
|
570
|
+
# 替换 [附件] 占位符为详细元信息
|
|
571
|
+
attach_idx = 0
|
|
572
|
+
|
|
573
|
+
def _replace_attachment_placeholder(match: re.Match) -> str:
|
|
574
|
+
nonlocal attach_idx
|
|
575
|
+
if attach_idx >= len(attachments):
|
|
576
|
+
return match.group(0)
|
|
577
|
+
att = attachments[attach_idx]
|
|
578
|
+
attach_idx += 1
|
|
579
|
+
size_str = _format_file_size(att['size'])
|
|
580
|
+
return (f"[附件: {att['name']} | {size_str} | "
|
|
581
|
+
f"{att['mimeType']} | fileId={att['fileId']}]")
|
|
582
|
+
|
|
583
|
+
modified_markdown = re.sub(
|
|
584
|
+
r'\[附件\]', _replace_attachment_placeholder, modified_markdown,
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
# 在 stderr 输出资源摘要
|
|
588
|
+
summary_lines: List[str] = []
|
|
589
|
+
if images:
|
|
590
|
+
ok = len(downloaded)
|
|
591
|
+
fail = len(failed)
|
|
592
|
+
summary_lines.append(f"图片: 发现 {len(images)} 个,成功下载 {ok} 个")
|
|
593
|
+
for item in downloaded:
|
|
594
|
+
size_str = _format_file_size(item['size'])
|
|
595
|
+
summary_lines.append(
|
|
596
|
+
f" ✓ {item['local_path']} ({size_str}, {item['mimeType']})")
|
|
597
|
+
for item in failed:
|
|
598
|
+
summary_lines.append(f" ✗ {item['url']} (原因: {item['error']})")
|
|
599
|
+
if attachments:
|
|
600
|
+
summary_lines.append(f"附件: 发现 {len(attachments)} 个")
|
|
601
|
+
for att in attachments:
|
|
602
|
+
size_str = _format_file_size(att['size'])
|
|
603
|
+
summary_lines.append(
|
|
604
|
+
f" - {att['name']} ({size_str}, {att['mimeType']})")
|
|
605
|
+
|
|
606
|
+
if summary_lines:
|
|
607
|
+
print("\n[资源摘要]", file=sys.stderr)
|
|
608
|
+
for line in summary_lines:
|
|
609
|
+
print(f" {line}", file=sys.stderr)
|
|
610
|
+
|
|
611
|
+
return modified_markdown, downloaded
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def upload_local_assets_in_markdown(
|
|
615
|
+
markdown: str,
|
|
616
|
+
base_dir: str,
|
|
617
|
+
) -> Tuple[str, List[Dict[str, str]]]:
|
|
618
|
+
"""
|
|
619
|
+
解析 markdown 中的本地图片/附件引用,通过 dodo_cli bos upload 上传到 BOS,
|
|
620
|
+
并替换为远程 URL。
|
|
621
|
+
|
|
622
|
+
:param markdown: 原始 markdown 文本
|
|
623
|
+
:param base_dir: 本地文件的基准目录(content-file 所在目录)
|
|
624
|
+
:return: (修改后的 markdown, 上传清单列表)
|
|
625
|
+
"""
|
|
626
|
+
# original_ref -> resolved_absolute_path
|
|
627
|
+
local_refs: Dict[str, str] = {}
|
|
628
|
+
|
|
629
|
+
def _collect_local_ref(ref: str) -> Optional[str]:
|
|
630
|
+
"""判断引用是否为本地文件,返回绝对路径。"""
|
|
631
|
+
ref = ref.strip()
|
|
632
|
+
# 跳过远程 URL、锚点、数据 URI
|
|
633
|
+
if ref.startswith(('http://', 'https://', '#', 'data:')):
|
|
634
|
+
return None
|
|
635
|
+
if ref in local_refs:
|
|
636
|
+
return local_refs[ref]
|
|
637
|
+
# 解析为绝对路径
|
|
638
|
+
if os.path.isabs(ref):
|
|
639
|
+
abs_path = ref
|
|
640
|
+
else:
|
|
641
|
+
abs_path = os.path.normpath(os.path.join(base_dir, ref))
|
|
642
|
+
# 检查文件是否存在
|
|
643
|
+
if not os.path.isfile(abs_path):
|
|
644
|
+
return None
|
|
645
|
+
local_refs[ref] = abs_path
|
|
646
|
+
return abs_path
|
|
647
|
+
|
|
648
|
+
# 扫描图片引用
|
|
649
|
+
for m in _MD_IMAGE_PATTERN.finditer(markdown):
|
|
650
|
+
ref = m.group(2).strip()
|
|
651
|
+
_collect_local_ref(ref)
|
|
652
|
+
|
|
653
|
+
# 扫描链接引用(只要本地文件存在就上传,不限制扩展名)
|
|
654
|
+
for m in _MD_LINK_PATTERN.finditer(markdown):
|
|
655
|
+
ref = m.group(2).strip()
|
|
656
|
+
if ref.startswith(('http://', 'https://', '#', 'data:')):
|
|
657
|
+
continue
|
|
658
|
+
_collect_local_ref(ref)
|
|
659
|
+
|
|
660
|
+
if not local_refs:
|
|
661
|
+
return markdown, []
|
|
662
|
+
|
|
663
|
+
# 上传资源
|
|
664
|
+
uploaded: List[Dict[str, str]] = []
|
|
665
|
+
failed: List[Dict[str, str]] = []
|
|
666
|
+
|
|
667
|
+
for original_ref, abs_path in local_refs.items():
|
|
668
|
+
try:
|
|
669
|
+
result = subprocess.run(
|
|
670
|
+
["dodo_cli", "bos", "upload", abs_path],
|
|
671
|
+
capture_output=True,
|
|
672
|
+
text=True,
|
|
673
|
+
timeout=60,
|
|
674
|
+
)
|
|
675
|
+
if result.returncode != 0:
|
|
676
|
+
raise RuntimeError(
|
|
677
|
+
f"dodo_cli bos upload 返回非零退出码 {result.returncode}: {result.stderr.strip()}"
|
|
678
|
+
)
|
|
679
|
+
# 从 dodo_cli 多行输出中解析 URL,格式如:
|
|
680
|
+
# Uploaded: local.txt -> uploads/...
|
|
681
|
+
# Bucket: dodo-dev
|
|
682
|
+
# Size: 1.2KB
|
|
683
|
+
# URL: https://...
|
|
684
|
+
bos_url = ""
|
|
685
|
+
for line in result.stdout.splitlines():
|
|
686
|
+
line = line.strip()
|
|
687
|
+
if line.startswith("URL:"):
|
|
688
|
+
bos_url = line[len("URL:"):].strip()
|
|
689
|
+
break
|
|
690
|
+
if not bos_url:
|
|
691
|
+
raise RuntimeError(
|
|
692
|
+
f"dodo_cli bos upload 输出中未找到 URL 行,完整输出:\n{result.stdout.strip()}"
|
|
693
|
+
)
|
|
694
|
+
uploaded.append({
|
|
695
|
+
'original_ref': original_ref,
|
|
696
|
+
'abs_path': abs_path,
|
|
697
|
+
'bos_url': bos_url,
|
|
698
|
+
})
|
|
699
|
+
except Exception as e:
|
|
700
|
+
failed.append({'original_ref': original_ref, 'error': str(e)})
|
|
701
|
+
|
|
702
|
+
# 替换 markdown 中的本地路径为 BOS URL
|
|
703
|
+
modified_markdown = markdown
|
|
704
|
+
for item in uploaded:
|
|
705
|
+
modified_markdown = modified_markdown.replace(item['original_ref'], item['bos_url'])
|
|
706
|
+
|
|
707
|
+
# 在 stderr 输出上传摘要
|
|
708
|
+
total_found = len(local_refs)
|
|
709
|
+
total_ok = len(uploaded)
|
|
710
|
+
total_fail = len(failed)
|
|
711
|
+
|
|
712
|
+
print(f"\n[资源上传] 共发现 {total_found} 个本地资源,成功上传 {total_ok} 个:", file=sys.stderr)
|
|
713
|
+
for item in uploaded:
|
|
714
|
+
print(f" - {item['original_ref']} -> {item['bos_url']}", file=sys.stderr)
|
|
715
|
+
if failed:
|
|
716
|
+
print(f" 上传失败 {total_fail} 个:", file=sys.stderr)
|
|
717
|
+
for item in failed:
|
|
718
|
+
print(f" - {item['original_ref']} (原因: {item['error']})", file=sys.stderr)
|
|
719
|
+
|
|
720
|
+
return modified_markdown, uploaded
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
# ---------------------------------------------------------------------------
|
|
724
|
+
# 目录格式化工具
|
|
725
|
+
# ---------------------------------------------------------------------------
|
|
726
|
+
|
|
727
|
+
def format_recent_views(
|
|
728
|
+
doc_id: str,
|
|
729
|
+
views: List[Dict[str, Any]],
|
|
730
|
+
total: int,
|
|
731
|
+
total_viewers: int,
|
|
732
|
+
page_num: int,
|
|
733
|
+
page_size: int,
|
|
734
|
+
) -> str:
|
|
735
|
+
"""将浏览记录格式化为 agent 友好的文本输出。"""
|
|
736
|
+
from datetime import datetime, timezone, timedelta
|
|
737
|
+
|
|
738
|
+
tz = timezone(timedelta(hours=8))
|
|
739
|
+
|
|
740
|
+
lines: List[str] = []
|
|
741
|
+
lines.append(f"文档 {doc_id} 浏览记录(共 {total} 次,{total_viewers} 位访客)")
|
|
742
|
+
|
|
743
|
+
if not views:
|
|
744
|
+
lines.append("")
|
|
745
|
+
lines.append("(暂无浏览记录)")
|
|
746
|
+
return "\n".join(lines)
|
|
747
|
+
|
|
748
|
+
lines.append("")
|
|
749
|
+
|
|
750
|
+
for v in views:
|
|
751
|
+
nickname = v.get("nickname", "")
|
|
752
|
+
username = v.get("username", "")
|
|
753
|
+
view_time = v.get("viewTime", 0)
|
|
754
|
+
time_str = datetime.fromtimestamp(view_time / 1000, tz=tz).strftime("%Y-%m-%d %H:%M")
|
|
755
|
+
lines.append(f"- {nickname} ({username}) · {time_str}")
|
|
756
|
+
|
|
757
|
+
# 分页提示
|
|
758
|
+
total_pages = (total + page_size - 1) // page_size if total > 0 else 1
|
|
759
|
+
if page_num < total_pages:
|
|
760
|
+
lines.append("")
|
|
761
|
+
lines.append(f"提示:还有更多记录,使用 --page {page_num + 1} 查看下一页。")
|
|
762
|
+
|
|
763
|
+
return "\n".join(lines)
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def format_comments(
|
|
767
|
+
doc_id: str,
|
|
768
|
+
bottom_comments: List[Dict[str, Any]],
|
|
769
|
+
side_comments: List[Dict[str, Any]],
|
|
770
|
+
bottom_total: int,
|
|
771
|
+
side_total: int,
|
|
772
|
+
page_num: int,
|
|
773
|
+
page_size: int,
|
|
774
|
+
) -> str:
|
|
775
|
+
"""将评论查询结果格式化为 agent 友好的文本输出。"""
|
|
776
|
+
from datetime import datetime, timezone, timedelta
|
|
777
|
+
|
|
778
|
+
tz = timezone(timedelta(hours=8))
|
|
779
|
+
|
|
780
|
+
def _fmt_time(ts_ms: int) -> str:
|
|
781
|
+
return datetime.fromtimestamp(ts_ms / 1000, tz=tz).strftime("%Y-%m-%d %H:%M")
|
|
782
|
+
|
|
783
|
+
def _fmt_comment(comment: Dict[str, Any], index: int, indent: str = "") -> List[str]:
|
|
784
|
+
user = comment.get("commentUserInfo") or {}
|
|
785
|
+
nickname = user.get("nickname", "")
|
|
786
|
+
username = user.get("username", "")
|
|
787
|
+
text = comment.get("text", "").strip()
|
|
788
|
+
created = comment.get("created", 0)
|
|
789
|
+
quote = comment.get("quote")
|
|
790
|
+
|
|
791
|
+
lines: List[str] = []
|
|
792
|
+
header = f"{indent}{index}. {nickname} ({username}) · {_fmt_time(created)}"
|
|
793
|
+
lines.append(header)
|
|
794
|
+
if quote:
|
|
795
|
+
lines.append(f'{indent} 引用: "{quote}"')
|
|
796
|
+
if text:
|
|
797
|
+
for tl in text.split("\n"):
|
|
798
|
+
lines.append(f"{indent} {tl}")
|
|
799
|
+
|
|
800
|
+
children = comment.get("childrenComments") or []
|
|
801
|
+
for ci, child in enumerate(children, 1):
|
|
802
|
+
c_user = child.get("commentUserInfo") or {}
|
|
803
|
+
c_nick = c_user.get("nickname", "")
|
|
804
|
+
c_uname = c_user.get("username", "")
|
|
805
|
+
c_text = child.get("text", "").strip()
|
|
806
|
+
c_time = child.get("created", 0)
|
|
807
|
+
is_last = ci == len(children)
|
|
808
|
+
prefix = "└─" if is_last else "├─"
|
|
809
|
+
cont = " " if is_last else "│ "
|
|
810
|
+
lines.append(f"{indent} {prefix} {c_nick} ({c_uname}) · {_fmt_time(c_time)}")
|
|
811
|
+
if c_text:
|
|
812
|
+
for tl in c_text.split("\n"):
|
|
813
|
+
lines.append(f"{indent} {cont} {tl}")
|
|
814
|
+
return lines
|
|
815
|
+
|
|
816
|
+
lines: List[str] = []
|
|
817
|
+
|
|
818
|
+
# 标题
|
|
819
|
+
parts: List[str] = []
|
|
820
|
+
if bottom_total >= 0:
|
|
821
|
+
parts.append(f"底部 {bottom_total} 条")
|
|
822
|
+
if side_total >= 0:
|
|
823
|
+
parts.append(f"划线 {side_total} 条")
|
|
824
|
+
total_str = ",".join(parts)
|
|
825
|
+
lines.append(f"文档 {doc_id} 评论({total_str})")
|
|
826
|
+
|
|
827
|
+
if bottom_total == 0 and side_total == 0:
|
|
828
|
+
lines.append("")
|
|
829
|
+
lines.append("(暂无评论)")
|
|
830
|
+
return "\n".join(lines)
|
|
831
|
+
|
|
832
|
+
# 底部评论
|
|
833
|
+
if bottom_comments:
|
|
834
|
+
lines.append("")
|
|
835
|
+
lines.append(f"## 底部评论({bottom_total} 条)")
|
|
836
|
+
lines.append("")
|
|
837
|
+
for i, c in enumerate(bottom_comments, 1):
|
|
838
|
+
lines.extend(_fmt_comment(c, i))
|
|
839
|
+
lines.append("")
|
|
840
|
+
|
|
841
|
+
# 划线评论
|
|
842
|
+
if side_comments:
|
|
843
|
+
lines.append("")
|
|
844
|
+
lines.append(f"## 划线评论({side_total} 条)")
|
|
845
|
+
lines.append("")
|
|
846
|
+
for i, c in enumerate(side_comments, 1):
|
|
847
|
+
lines.extend(_fmt_comment(c, i))
|
|
848
|
+
lines.append("")
|
|
849
|
+
|
|
850
|
+
# 分页提示
|
|
851
|
+
total = max(bottom_total, side_total)
|
|
852
|
+
total_pages = (total + page_size - 1) // page_size if total > 0 else 1
|
|
853
|
+
if page_num < total_pages:
|
|
854
|
+
lines.append(f"提示:还有更多评论,使用 --page {page_num + 1} 查看下一页。")
|
|
855
|
+
|
|
856
|
+
return "\n".join(lines)
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
def format_repo_tree(
|
|
860
|
+
repo_guid: str,
|
|
861
|
+
data: List[Dict[str, Any]],
|
|
862
|
+
page_num: int,
|
|
863
|
+
total: int,
|
|
864
|
+
parent_doc_guid: str,
|
|
865
|
+
) -> str:
|
|
866
|
+
"""将知识库目录查询结果格式化为文本树形输出。"""
|
|
867
|
+
lines: List[str] = []
|
|
868
|
+
|
|
869
|
+
# ---- 标题行 ----
|
|
870
|
+
is_root = (parent_doc_guid == KuApiClient.ROOT_DOC_GUID)
|
|
871
|
+
location = "根目录" if is_root else f"目录 {parent_doc_guid}"
|
|
872
|
+
page_size = KuApiClient.REPO_PAGE_SIZE
|
|
873
|
+
total_pages = (total + page_size - 1) // page_size if total > 0 else 1
|
|
874
|
+
|
|
875
|
+
if total <= page_size:
|
|
876
|
+
lines.append(f"知识库 {repo_guid} {location}(共 {total} 项):")
|
|
877
|
+
else:
|
|
878
|
+
lines.append(
|
|
879
|
+
f"知识库 {repo_guid} {location}"
|
|
880
|
+
f"(第 {page_num} 页,当前 {len(data)} 项"
|
|
881
|
+
f" / 共 {total} 项):"
|
|
882
|
+
)
|
|
883
|
+
lines.append("")
|
|
884
|
+
|
|
885
|
+
# ---- 文档列表 ----
|
|
886
|
+
if not data:
|
|
887
|
+
lines.append("(空目录)")
|
|
888
|
+
else:
|
|
889
|
+
for doc in data:
|
|
890
|
+
child_count = doc.get("childCount", 0)
|
|
891
|
+
icon = "📁" if child_count > 0 else "📄"
|
|
892
|
+
name = doc.get("name", "未命名")
|
|
893
|
+
doc_guid = doc.get("docGuid", "")
|
|
894
|
+
line = f"{icon} {name} ({doc_guid})"
|
|
895
|
+
if child_count > 0:
|
|
896
|
+
line += f" [{child_count} 子文档]"
|
|
897
|
+
lines.append(line)
|
|
898
|
+
|
|
899
|
+
# ---- 翻页提示 ----
|
|
900
|
+
if total > page_size and page_num < total_pages:
|
|
901
|
+
lines.append("")
|
|
902
|
+
lines.append(f"提示:还有更多内容,使用 --page {page_num + 1} 查看下一页。")
|
|
903
|
+
|
|
904
|
+
return "\n".join(lines)
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
# ---------------------------------------------------------------------------
|
|
908
|
+
# CLI
|
|
909
|
+
# ---------------------------------------------------------------------------
|
|
910
|
+
|
|
911
|
+
def _get_ugate_token(username: str) -> str:
|
|
912
|
+
"""调用 ugate-auth skill 获取 ugate token。"""
|
|
913
|
+
script_path = Path(__file__).parent.parent.parent / "ugate-auth" / "scripts" / "getUgateToken.py"
|
|
914
|
+
if not script_path.exists():
|
|
915
|
+
print(f"错误: 未找到 ugate-auth 脚本: {script_path}", file=sys.stderr)
|
|
916
|
+
sys.exit(1)
|
|
917
|
+
result = subprocess.run(
|
|
918
|
+
[sys.executable, str(script_path), username],
|
|
919
|
+
capture_output=True, text=True, timeout=30,
|
|
920
|
+
)
|
|
921
|
+
if result.returncode == 0:
|
|
922
|
+
return result.stdout.strip()
|
|
923
|
+
if result.returncode == 2:
|
|
924
|
+
# 需要用户授权
|
|
925
|
+
print(result.stderr, file=sys.stderr, end="")
|
|
926
|
+
sys.exit(2)
|
|
927
|
+
# 其他错误
|
|
928
|
+
print(result.stderr, file=sys.stderr, end="")
|
|
929
|
+
sys.exit(1)
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
def _get_client() -> KuApiClient:
|
|
933
|
+
"""通过 ugate-auth 获取 token 并构造 API 客户端。"""
|
|
934
|
+
username = os.environ.get("COMATE_USERNAME", "")
|
|
935
|
+
if not username:
|
|
936
|
+
print("错误: 未设置环境变量 COMATE_USERNAME", file=sys.stderr)
|
|
937
|
+
sys.exit(1)
|
|
938
|
+
token = _get_ugate_token(username)
|
|
939
|
+
return KuApiClient(DEFAULT_API_BASE, token)
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
def _output_json(data: Any) -> None:
|
|
943
|
+
"""将结果以格式化 JSON 输出到 stdout。"""
|
|
944
|
+
print(json.dumps(data, indent=2, ensure_ascii=False))
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
def _check_result(response: Dict[str, Any], allow_empty: bool = False) -> Dict[str, Any]:
|
|
948
|
+
"""从 API 响应中提取 result 字段,处理 null / 错误情况。
|
|
949
|
+
|
|
950
|
+
API 响应格式: {"returnCode": 200, "result": {...}, "success": true}
|
|
951
|
+
当 API 返回错误时 result 可能为 null,此时 .get("result", {}) 返回 None 而非 {}。
|
|
952
|
+
|
|
953
|
+
:param response: API 原始响应字典
|
|
954
|
+
:param allow_empty: 若为 True,result 为 None 时返回空字典而不退出
|
|
955
|
+
:return: result 字典(保证非 None)
|
|
956
|
+
"""
|
|
957
|
+
inner = response.get("result")
|
|
958
|
+
if inner is None:
|
|
959
|
+
if allow_empty:
|
|
960
|
+
return {}
|
|
961
|
+
msg = (response.get("returnMessage")
|
|
962
|
+
or response.get("msg")
|
|
963
|
+
or f"API 返回错误 (returnCode={response.get('returnCode')})")
|
|
964
|
+
print(f"错误: {msg}", file=sys.stderr)
|
|
965
|
+
sys.exit(1)
|
|
966
|
+
return inner
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
def cmd_query_content(args: argparse.Namespace) -> None:
|
|
970
|
+
"""
|
|
971
|
+
查询知识库文档内容
|
|
972
|
+
"""
|
|
973
|
+
client = _get_client()
|
|
974
|
+
result = client.query_content(doc_id=args.doc_id, url=args.url)
|
|
975
|
+
inner = _check_result(result)
|
|
976
|
+
content_blocks = inner.get("content", [])
|
|
977
|
+
|
|
978
|
+
if args.raw_json:
|
|
979
|
+
_output_json(content_blocks)
|
|
980
|
+
return
|
|
981
|
+
|
|
982
|
+
markdown = convert_blocks_to_markdown(content_blocks)
|
|
983
|
+
|
|
984
|
+
# 资源下载处理
|
|
985
|
+
if not args.no_download_assets:
|
|
986
|
+
# 确定文档 ID(优先用 doc_id,否则从 URL 提取最后一段)
|
|
987
|
+
doc_id = args.doc_id
|
|
988
|
+
if not doc_id and args.url:
|
|
989
|
+
doc_id = args.url.rstrip('/').split('/')[-1]
|
|
990
|
+
if not doc_id:
|
|
991
|
+
doc_id = "unknown"
|
|
992
|
+
markdown, _ = download_assets_from_blocks(
|
|
993
|
+
markdown, content_blocks, doc_id,
|
|
994
|
+
save_dir=args.save_assets_dir,
|
|
995
|
+
)
|
|
996
|
+
|
|
997
|
+
print(markdown)
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
def cmd_query_repo(args: argparse.Namespace) -> None:
|
|
1001
|
+
"""
|
|
1002
|
+
查询 repo 目录结构
|
|
1003
|
+
"""
|
|
1004
|
+
client = _get_client()
|
|
1005
|
+
parent_doc_guid = args.parent_doc_guid or KuApiClient.ROOT_DOC_GUID
|
|
1006
|
+
page_num = args.page or 1
|
|
1007
|
+
result = client.query_repo(
|
|
1008
|
+
repo_guid=args.repo_guid,
|
|
1009
|
+
parent_doc_guid=parent_doc_guid,
|
|
1010
|
+
page_num=page_num,
|
|
1011
|
+
)
|
|
1012
|
+
# 从响应中提取数据
|
|
1013
|
+
result_body = _check_result(result, allow_empty=True)
|
|
1014
|
+
if not result_body:
|
|
1015
|
+
print("数据不存在")
|
|
1016
|
+
else:
|
|
1017
|
+
data = result_body.get("data", [])
|
|
1018
|
+
total = result_body.get("total", 0)
|
|
1019
|
+
output = format_repo_tree(
|
|
1020
|
+
repo_guid=args.repo_guid,
|
|
1021
|
+
data=data,
|
|
1022
|
+
page_num=page_num,
|
|
1023
|
+
total=total,
|
|
1024
|
+
parent_doc_guid=parent_doc_guid,
|
|
1025
|
+
)
|
|
1026
|
+
print(output)
|
|
1027
|
+
|
|
1028
|
+
|
|
1029
|
+
def cmd_create_doc(args: argparse.Namespace) -> None:
|
|
1030
|
+
"""
|
|
1031
|
+
创建一个文档
|
|
1032
|
+
"""
|
|
1033
|
+
client = _get_client()
|
|
1034
|
+
# 从文件读取内容
|
|
1035
|
+
content = None
|
|
1036
|
+
if args.content_file:
|
|
1037
|
+
with open(args.content_file, "r", encoding="utf-8") as f:
|
|
1038
|
+
content = f.read()
|
|
1039
|
+
# 自动上传本地图片/附件到 BOS,替换为远程 URL
|
|
1040
|
+
base_dir = os.path.dirname(os.path.abspath(args.content_file))
|
|
1041
|
+
content, _ = upload_local_assets_in_markdown(content, base_dir)
|
|
1042
|
+
result = client.create_doc(
|
|
1043
|
+
repository_guid=args.repo_guid,
|
|
1044
|
+
title=args.title,
|
|
1045
|
+
content=content,
|
|
1046
|
+
parent_doc_guid=args.parent_doc_guid,
|
|
1047
|
+
)
|
|
1048
|
+
_output_json(result)
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
def cmd_move_doc(args: argparse.Namespace) -> None:
|
|
1052
|
+
"""
|
|
1053
|
+
移动文档的位置
|
|
1054
|
+
"""
|
|
1055
|
+
client = _get_client()
|
|
1056
|
+
result = client.move_doc(
|
|
1057
|
+
doc_id=args.doc_id,
|
|
1058
|
+
to_repo_guid=args.to_repo_guid,
|
|
1059
|
+
to_parent_guid=args.to_parent_guid,
|
|
1060
|
+
)
|
|
1061
|
+
_output_json(result)
|
|
1062
|
+
|
|
1063
|
+
|
|
1064
|
+
def cmd_delete_doc(args: argparse.Namespace) -> None:
|
|
1065
|
+
"""
|
|
1066
|
+
删除文档
|
|
1067
|
+
"""
|
|
1068
|
+
client = _get_client()
|
|
1069
|
+
result = client.delete_doc(doc_id=args.doc_id)
|
|
1070
|
+
_output_json(result)
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
def cmd_query_user_info(args: argparse.Namespace) -> None:
|
|
1074
|
+
"""
|
|
1075
|
+
查询当前用户信息
|
|
1076
|
+
"""
|
|
1077
|
+
username = os.environ.get("COMATE_USERNAME", "")
|
|
1078
|
+
if not username:
|
|
1079
|
+
print("错误: 未设置环境变量 COMATE_USERNAME", file=sys.stderr)
|
|
1080
|
+
sys.exit(1)
|
|
1081
|
+
client = _get_client()
|
|
1082
|
+
result = client.query_user_info(username=username)
|
|
1083
|
+
inner = _check_result(result)
|
|
1084
|
+
repo = inner.get("userPersonalRepo") or {}
|
|
1085
|
+
output = {
|
|
1086
|
+
"username": inner.get("username", ""),
|
|
1087
|
+
"nickname": inner.get("nickname", ""),
|
|
1088
|
+
"email": inner.get("email", ""),
|
|
1089
|
+
"personalRepoGuid": repo.get("repositoryGuid", ""),
|
|
1090
|
+
"personalRepoUrl": repo.get("url", ""),
|
|
1091
|
+
}
|
|
1092
|
+
_output_json(output)
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
def cmd_copy_doc(args: argparse.Namespace) -> None:
|
|
1096
|
+
"""复制文档。"""
|
|
1097
|
+
client = _get_client()
|
|
1098
|
+
result = client.copy_doc(
|
|
1099
|
+
doc_id=args.doc_id,
|
|
1100
|
+
to_repo_guid=args.to_repo_guid,
|
|
1101
|
+
to_parent_guid=args.to_parent_guid,
|
|
1102
|
+
new_title=args.new_title,
|
|
1103
|
+
)
|
|
1104
|
+
_output_json(result)
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
def cmd_edit_content(args: argparse.Namespace) -> None:
|
|
1108
|
+
"""编辑文档正文。"""
|
|
1109
|
+
with open(args.operations_file, "r", encoding="utf-8") as f:
|
|
1110
|
+
operations = json.load(f)
|
|
1111
|
+
client = _get_client()
|
|
1112
|
+
result = client.edit_content(
|
|
1113
|
+
doc_id=args.doc_id,
|
|
1114
|
+
operations=operations,
|
|
1115
|
+
publish=args.publish,
|
|
1116
|
+
)
|
|
1117
|
+
_output_json(result)
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
def cmd_upload_attachment(args: argparse.Namespace) -> None:
|
|
1121
|
+
"""上传附件到文档正文。"""
|
|
1122
|
+
if not os.path.isfile(args.file):
|
|
1123
|
+
print(f"错误: 文件不存在: {args.file}", file=sys.stderr)
|
|
1124
|
+
sys.exit(1)
|
|
1125
|
+
client = _get_client()
|
|
1126
|
+
result = client.upload_attachment(doc_id=args.doc_id, file_path=args.file)
|
|
1127
|
+
|
|
1128
|
+
# 默认自动插入正文
|
|
1129
|
+
if not args.no_insert:
|
|
1130
|
+
attach_result = _check_result(result, allow_empty=True)
|
|
1131
|
+
attach_id = attach_result.get("attachId", "")
|
|
1132
|
+
attach_name = attach_result.get("name", "")
|
|
1133
|
+
attach_ext = attach_result.get("extension", "")
|
|
1134
|
+
attach_size = attach_result.get("size", 0)
|
|
1135
|
+
if attach_id:
|
|
1136
|
+
import mimetypes
|
|
1137
|
+
mime_type = mimetypes.guess_type(args.file)[0] or "application/octet-stream"
|
|
1138
|
+
view_type = args.view_type or "attachment"
|
|
1139
|
+
operations = [{
|
|
1140
|
+
"mode": "sibling",
|
|
1141
|
+
"json": [{
|
|
1142
|
+
"type": "attachment",
|
|
1143
|
+
"children": [{"text": ""}],
|
|
1144
|
+
"fileId": attach_id,
|
|
1145
|
+
"docId": args.doc_id,
|
|
1146
|
+
"fileInfo": {
|
|
1147
|
+
"name": attach_name,
|
|
1148
|
+
"extension": attach_ext,
|
|
1149
|
+
"size": attach_size,
|
|
1150
|
+
"type": mime_type,
|
|
1151
|
+
},
|
|
1152
|
+
"url": "",
|
|
1153
|
+
"invalid": False,
|
|
1154
|
+
"viewType": view_type,
|
|
1155
|
+
}],
|
|
1156
|
+
}]
|
|
1157
|
+
client.edit_content(doc_id=args.doc_id, operations=operations)
|
|
1158
|
+
print(f"附件已上传并插入文档正文 (attachId={attach_id})", file=sys.stderr)
|
|
1159
|
+
|
|
1160
|
+
_output_json(result)
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
def _discover_diagrams(client: KuApiClient, doc_id: str) -> List[Dict[str, Any]]:
|
|
1164
|
+
"""从文档正文中发现所有流程图节点。
|
|
1165
|
+
|
|
1166
|
+
调用 queryContent 获取文档 block 列表,筛选 type=diagram 的节点,
|
|
1167
|
+
返回 [{id, blockId, boxSize}, ...] 列表。
|
|
1168
|
+
"""
|
|
1169
|
+
result = client.query_content(doc_id=doc_id)
|
|
1170
|
+
inner = _check_result(result)
|
|
1171
|
+
blocks = inner.get("content", [])
|
|
1172
|
+
diagrams = []
|
|
1173
|
+
for idx, block in enumerate(blocks):
|
|
1174
|
+
if block.get("type") == "diagram":
|
|
1175
|
+
diagrams.append({
|
|
1176
|
+
"index": idx,
|
|
1177
|
+
"flowchartId": block.get("id", ""),
|
|
1178
|
+
"blockId": block.get("blockId", ""),
|
|
1179
|
+
"boxSize": block.get("boxSize"),
|
|
1180
|
+
})
|
|
1181
|
+
return diagrams
|
|
1182
|
+
|
|
1183
|
+
|
|
1184
|
+
def _parse_mxgraph_xml(xml_str: str) -> List[Dict[str, str]]:
|
|
1185
|
+
"""从 mxGraph XML 中提取节点和边的信息,返回简要描述列表。"""
|
|
1186
|
+
import xml.etree.ElementTree as ET
|
|
1187
|
+
items: List[Dict[str, str]] = []
|
|
1188
|
+
try:
|
|
1189
|
+
root = ET.fromstring(xml_str)
|
|
1190
|
+
except ET.ParseError:
|
|
1191
|
+
return items
|
|
1192
|
+
for cell in root.iter("mxCell"):
|
|
1193
|
+
cell_id = cell.get("id", "")
|
|
1194
|
+
# 跳过根容器节点(id=0 或 id=1)
|
|
1195
|
+
if cell_id in ("0", "1"):
|
|
1196
|
+
continue
|
|
1197
|
+
value = cell.get("value", "")
|
|
1198
|
+
style = cell.get("style", "")
|
|
1199
|
+
source = cell.get("source")
|
|
1200
|
+
target = cell.get("target")
|
|
1201
|
+
if source and target:
|
|
1202
|
+
item = {"type": "edge", "id": cell_id,
|
|
1203
|
+
"source": source, "target": target}
|
|
1204
|
+
if value:
|
|
1205
|
+
item["label"] = value
|
|
1206
|
+
items.append(item)
|
|
1207
|
+
else:
|
|
1208
|
+
# 解析形状类型
|
|
1209
|
+
shape = "node"
|
|
1210
|
+
if "rhombus" in style:
|
|
1211
|
+
shape = "diamond (判断)"
|
|
1212
|
+
elif "process" in style:
|
|
1213
|
+
shape = "process (处理)"
|
|
1214
|
+
elif "ellipse" in style:
|
|
1215
|
+
shape = "ellipse (开始/结束)"
|
|
1216
|
+
elif "rounded" in style and "edge" not in style:
|
|
1217
|
+
shape = "rounded-rect"
|
|
1218
|
+
item = {"type": "node", "id": cell_id, "shape": shape}
|
|
1219
|
+
if value:
|
|
1220
|
+
item["label"] = value
|
|
1221
|
+
items.append(item)
|
|
1222
|
+
return items
|
|
1223
|
+
|
|
1224
|
+
|
|
1225
|
+
def _format_flowchart_output(
|
|
1226
|
+
doc_id: str,
|
|
1227
|
+
flowchart_id: str,
|
|
1228
|
+
content_xml: str,
|
|
1229
|
+
index: int = 0,
|
|
1230
|
+
) -> str:
|
|
1231
|
+
"""将流程图数据格式化为 agent 友好的文本输出。"""
|
|
1232
|
+
lines: List[str] = []
|
|
1233
|
+
lines.append(f"## 流程图 #{index + 1}")
|
|
1234
|
+
lines.append(f"- 文档 ID: {doc_id}")
|
|
1235
|
+
lines.append(f"- 流程图 ID: {flowchart_id}")
|
|
1236
|
+
lines.append(f"- 格式: mxGraph XML")
|
|
1237
|
+
lines.append("")
|
|
1238
|
+
|
|
1239
|
+
# 解析节点和边
|
|
1240
|
+
items = _parse_mxgraph_xml(content_xml)
|
|
1241
|
+
nodes = [it for it in items if it["type"] == "node"]
|
|
1242
|
+
edges = [it for it in items if it["type"] == "edge"]
|
|
1243
|
+
|
|
1244
|
+
if nodes:
|
|
1245
|
+
lines.append(f"### 节点 ({len(nodes)} 个)")
|
|
1246
|
+
for n in nodes:
|
|
1247
|
+
label = n.get("label", "(无文字)")
|
|
1248
|
+
lines.append(f" - [{n['id']}] {n.get('shape', 'node')}: {label}")
|
|
1249
|
+
if edges:
|
|
1250
|
+
lines.append(f"### 连线 ({len(edges)} 个)")
|
|
1251
|
+
for e in edges:
|
|
1252
|
+
label = f" ({e['label']})" if e.get("label") else ""
|
|
1253
|
+
lines.append(f" - [{e['id']}] {e['source']} → {e['target']}{label}")
|
|
1254
|
+
|
|
1255
|
+
if not nodes and not edges:
|
|
1256
|
+
lines.append("(流程图为空或格式无法解析)")
|
|
1257
|
+
|
|
1258
|
+
lines.append("")
|
|
1259
|
+
lines.append("### 原始 XML")
|
|
1260
|
+
lines.append("```xml")
|
|
1261
|
+
lines.append(content_xml)
|
|
1262
|
+
lines.append("```")
|
|
1263
|
+
|
|
1264
|
+
return "\n".join(lines)
|
|
1265
|
+
|
|
1266
|
+
|
|
1267
|
+
def cmd_query_flowchart(args: argparse.Namespace) -> None:
|
|
1268
|
+
"""查询文档流程图数据。
|
|
1269
|
+
|
|
1270
|
+
当 --flowchart-id 未指定时,自动从文档正文中发现所有流程图并逐一查询。
|
|
1271
|
+
"""
|
|
1272
|
+
client = _get_client()
|
|
1273
|
+
doc_id = args.doc_id
|
|
1274
|
+
|
|
1275
|
+
# 确定要查询的流程图列表
|
|
1276
|
+
if args.flowchart_id:
|
|
1277
|
+
targets = [{"flowchartId": args.flowchart_id, "index": 0}]
|
|
1278
|
+
else:
|
|
1279
|
+
diagrams = _discover_diagrams(client, doc_id)
|
|
1280
|
+
if not diagrams:
|
|
1281
|
+
print(f"文档 {doc_id} 中未发现流程图(type=diagram 的内容块)。",
|
|
1282
|
+
file=sys.stderr)
|
|
1283
|
+
sys.exit(0)
|
|
1284
|
+
print(f"发现 {len(diagrams)} 个流程图,正在查询...", file=sys.stderr)
|
|
1285
|
+
targets = diagrams
|
|
1286
|
+
|
|
1287
|
+
# 逐一查询并输出
|
|
1288
|
+
outputs: List[str] = []
|
|
1289
|
+
for i, target in enumerate(targets):
|
|
1290
|
+
fid = target["flowchartId"]
|
|
1291
|
+
result = client.query_flowchart(doc_id=doc_id, flowchart_id=fid)
|
|
1292
|
+
inner = _check_result(result, allow_empty=True)
|
|
1293
|
+
content_xml = inner.get("content", "") if isinstance(inner, dict) else ""
|
|
1294
|
+
|
|
1295
|
+
if args.output:
|
|
1296
|
+
# 多个流程图时文件名加序号
|
|
1297
|
+
out_path = args.output
|
|
1298
|
+
if len(targets) > 1:
|
|
1299
|
+
base, ext = os.path.splitext(args.output)
|
|
1300
|
+
out_path = f"{base}_{i + 1}{ext}"
|
|
1301
|
+
with open(out_path, "w", encoding="utf-8") as f:
|
|
1302
|
+
f.write(content_xml)
|
|
1303
|
+
print(f"流程图 #{i + 1} (id={fid}) 已保存到: {out_path}")
|
|
1304
|
+
else:
|
|
1305
|
+
outputs.append(
|
|
1306
|
+
_format_flowchart_output(doc_id, fid, content_xml, index=i)
|
|
1307
|
+
)
|
|
1308
|
+
|
|
1309
|
+
if outputs:
|
|
1310
|
+
print("\n---\n".join(outputs))
|
|
1311
|
+
|
|
1312
|
+
|
|
1313
|
+
def cmd_add_member(args: argparse.Namespace) -> None:
|
|
1314
|
+
"""添加文档成员。"""
|
|
1315
|
+
usernames = [u.strip() for u in args.usernames.split(",") if u.strip()]
|
|
1316
|
+
role = "DocMember" if args.role == "DocEditor" else args.role
|
|
1317
|
+
client = _get_client()
|
|
1318
|
+
result = client.add_member(
|
|
1319
|
+
doc_id=args.doc_id,
|
|
1320
|
+
usernames=usernames,
|
|
1321
|
+
role=role,
|
|
1322
|
+
)
|
|
1323
|
+
_output_json(result)
|
|
1324
|
+
|
|
1325
|
+
|
|
1326
|
+
def cmd_update_member(args: argparse.Namespace) -> None:
|
|
1327
|
+
"""更新文档成员角色。"""
|
|
1328
|
+
role = "DocMember" if args.role == "DocEditor" else args.role
|
|
1329
|
+
client = _get_client()
|
|
1330
|
+
result = client.update_member(
|
|
1331
|
+
doc_id=args.doc_id,
|
|
1332
|
+
username=args.username,
|
|
1333
|
+
role=role,
|
|
1334
|
+
)
|
|
1335
|
+
_output_json(result)
|
|
1336
|
+
|
|
1337
|
+
|
|
1338
|
+
def cmd_change_scope(args: argparse.Namespace) -> None:
|
|
1339
|
+
"""修改文档公开/私密范围。"""
|
|
1340
|
+
client = _get_client()
|
|
1341
|
+
result = client.change_scope(doc_id=args.doc_id, scope=args.scope)
|
|
1342
|
+
_output_json(result)
|
|
1343
|
+
|
|
1344
|
+
|
|
1345
|
+
def cmd_query_permission(args: argparse.Namespace) -> None:
|
|
1346
|
+
"""查询用户文档权限。"""
|
|
1347
|
+
usernames = [u.strip() for u in args.usernames.split(",") if u.strip()]
|
|
1348
|
+
client = _get_client()
|
|
1349
|
+
result = client.query_permission(doc_id=args.doc_id, usernames=usernames)
|
|
1350
|
+
_output_json(result)
|
|
1351
|
+
|
|
1352
|
+
|
|
1353
|
+
def cmd_query_comment(args: argparse.Namespace) -> None:
|
|
1354
|
+
"""查询文档评论。"""
|
|
1355
|
+
client = _get_client()
|
|
1356
|
+
query_type = args.type
|
|
1357
|
+
bottom = query_type in ("all", "bottom")
|
|
1358
|
+
side = query_type in ("all", "side")
|
|
1359
|
+
result = client.query_comment(
|
|
1360
|
+
doc_id=args.doc_id,
|
|
1361
|
+
bottom=bottom,
|
|
1362
|
+
side=side,
|
|
1363
|
+
page=args.page,
|
|
1364
|
+
page_size=args.page_size,
|
|
1365
|
+
)
|
|
1366
|
+
|
|
1367
|
+
if args.raw_json:
|
|
1368
|
+
_output_json(result)
|
|
1369
|
+
return
|
|
1370
|
+
|
|
1371
|
+
inner = _check_result(result, allow_empty=True)
|
|
1372
|
+
bottom_result = inner.get("bottomCommentResult") or {}
|
|
1373
|
+
side_result = inner.get("sideCommentResult") or {}
|
|
1374
|
+
|
|
1375
|
+
output = format_comments(
|
|
1376
|
+
doc_id=args.doc_id,
|
|
1377
|
+
bottom_comments=bottom_result.get("comments", []),
|
|
1378
|
+
side_comments=side_result.get("comments", []),
|
|
1379
|
+
bottom_total=bottom_result.get("totalCount", 0) if bottom else -1,
|
|
1380
|
+
side_total=side_result.get("totalCount", 0) if side else -1,
|
|
1381
|
+
page_num=args.page,
|
|
1382
|
+
page_size=args.page_size,
|
|
1383
|
+
)
|
|
1384
|
+
print(output)
|
|
1385
|
+
|
|
1386
|
+
|
|
1387
|
+
def cmd_query_recent_view(args: argparse.Namespace) -> None:
|
|
1388
|
+
"""查询文档浏览记录。"""
|
|
1389
|
+
from datetime import datetime, timezone, timedelta
|
|
1390
|
+
|
|
1391
|
+
tz = timezone(timedelta(hours=8))
|
|
1392
|
+
|
|
1393
|
+
def _parse_time(s: Optional[str]) -> Optional[int]:
|
|
1394
|
+
if not s:
|
|
1395
|
+
return None
|
|
1396
|
+
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%d", "%Y-%m-%dT%H:%M:%S"):
|
|
1397
|
+
try:
|
|
1398
|
+
dt = datetime.strptime(s, fmt).replace(tzinfo=tz)
|
|
1399
|
+
return int(dt.timestamp() * 1000)
|
|
1400
|
+
except ValueError:
|
|
1401
|
+
continue
|
|
1402
|
+
print(f"错误: 无法解析时间 '{s}',支持格式: YYYY-MM-DD, YYYY-MM-DD HH:MM, YYYY-MM-DD HH:MM:SS",
|
|
1403
|
+
file=sys.stderr)
|
|
1404
|
+
sys.exit(1)
|
|
1405
|
+
|
|
1406
|
+
client = _get_client()
|
|
1407
|
+
result = client.query_recent_view(
|
|
1408
|
+
doc_id=args.doc_id,
|
|
1409
|
+
begin_time=_parse_time(args.begin_time),
|
|
1410
|
+
end_time=_parse_time(args.end_time),
|
|
1411
|
+
page=args.page,
|
|
1412
|
+
page_size=args.page_size,
|
|
1413
|
+
)
|
|
1414
|
+
|
|
1415
|
+
if args.raw_json:
|
|
1416
|
+
_output_json(result)
|
|
1417
|
+
return
|
|
1418
|
+
|
|
1419
|
+
inner = _check_result(result, allow_empty=True)
|
|
1420
|
+
output = format_recent_views(
|
|
1421
|
+
doc_id=args.doc_id,
|
|
1422
|
+
views=inner.get("data", []),
|
|
1423
|
+
total=inner.get("total", 0),
|
|
1424
|
+
total_viewers=inner.get("totalViewers", 0),
|
|
1425
|
+
page_num=args.page,
|
|
1426
|
+
page_size=args.page_size,
|
|
1427
|
+
)
|
|
1428
|
+
print(output)
|
|
1429
|
+
|
|
1430
|
+
|
|
1431
|
+
def main() -> None:
|
|
1432
|
+
"""
|
|
1433
|
+
主入口函数
|
|
1434
|
+
"""
|
|
1435
|
+
parser = argparse.ArgumentParser(
|
|
1436
|
+
description="知识库(Ku)文档操作 CLI 工具",
|
|
1437
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
1438
|
+
)
|
|
1439
|
+
subparsers = parser.add_subparsers(dest="command", help="可用子命令")
|
|
1440
|
+
|
|
1441
|
+
# ---- query-content ----
|
|
1442
|
+
p_qc = subparsers.add_parser("query-content", help="查询文档内容(输出 Markdown 格式)")
|
|
1443
|
+
p_qc.add_argument("--doc-id", help="文档 ID")
|
|
1444
|
+
p_qc.add_argument("--url", help="文档 URL")
|
|
1445
|
+
p_qc.add_argument("--save-assets-dir", default="./ku_assets",
|
|
1446
|
+
help="资源保存目录(默认 ./ku_assets)")
|
|
1447
|
+
p_qc.add_argument("--no-download-assets", action="store_true",
|
|
1448
|
+
help="禁用自动下载图片/附件")
|
|
1449
|
+
p_qc.add_argument("--raw-json", action="store_true",
|
|
1450
|
+
help="输出原始 JSON block 结构(含 blockId),不转换为 Markdown")
|
|
1451
|
+
p_qc.set_defaults(func=cmd_query_content)
|
|
1452
|
+
|
|
1453
|
+
# ---- query-repo ----
|
|
1454
|
+
p_qr = subparsers.add_parser("query-repo", help="查询知识库目录(文本树形输出)")
|
|
1455
|
+
p_qr.add_argument("--repo-guid", required=True, help="知识库 ID(必填)")
|
|
1456
|
+
p_qr.add_argument("--parent-doc-guid", help="父文档 ID(默认查询根目录)")
|
|
1457
|
+
p_qr.add_argument("--page", type=int, default=1, help="页码(默认 1,每页 100 条)")
|
|
1458
|
+
p_qr.set_defaults(func=cmd_query_repo)
|
|
1459
|
+
|
|
1460
|
+
# ---- create-doc ----
|
|
1461
|
+
p_cd = subparsers.add_parser("create-doc", help="新建文档/目录")
|
|
1462
|
+
p_cd.add_argument("--repo-guid", required=True, help="知识库 ID(必填)")
|
|
1463
|
+
p_cd.add_argument("--title", help="文档标题")
|
|
1464
|
+
p_cd.add_argument("--content-file", help="从本地文件读取文档内容(Markdown 格式)")
|
|
1465
|
+
p_cd.add_argument("--parent-doc-guid", help="父文档 ID(不传则创建在根目录)")
|
|
1466
|
+
p_cd.set_defaults(func=cmd_create_doc)
|
|
1467
|
+
|
|
1468
|
+
# ---- move-doc ----
|
|
1469
|
+
p_md = subparsers.add_parser("move-doc", help="移动文档")
|
|
1470
|
+
p_md.add_argument("--doc-id", required=True, help="文档 ID(必填)")
|
|
1471
|
+
p_md.add_argument("--to-repo-guid", required=True, help="目标知识库 ID(必填)")
|
|
1472
|
+
p_md.add_argument("--to-parent-guid", help="目标父目录 ID(不传则移动到根目录)")
|
|
1473
|
+
p_md.set_defaults(func=cmd_move_doc)
|
|
1474
|
+
|
|
1475
|
+
# ---- delete-doc ----
|
|
1476
|
+
p_dd = subparsers.add_parser("delete-doc", help="删除文档")
|
|
1477
|
+
p_dd.add_argument("--doc-id", required=True, help="文档 ID(必填)")
|
|
1478
|
+
p_dd.set_defaults(func=cmd_delete_doc)
|
|
1479
|
+
|
|
1480
|
+
# ---- query-user-info ----
|
|
1481
|
+
p_qui = subparsers.add_parser("query-user-info", help="查询当前用户信息")
|
|
1482
|
+
p_qui.set_defaults(func=cmd_query_user_info)
|
|
1483
|
+
|
|
1484
|
+
# ---- copy-doc ----
|
|
1485
|
+
p_cp = subparsers.add_parser("copy-doc", help="复制文档")
|
|
1486
|
+
p_cp.add_argument("--doc-id", required=True, help="源文档 ID(必填)")
|
|
1487
|
+
p_cp.add_argument("--to-repo-guid", help="目标知识库 ID")
|
|
1488
|
+
p_cp.add_argument("--to-parent-guid", help="目标父目录 ID")
|
|
1489
|
+
p_cp.add_argument("--new-title", help="新文档标题")
|
|
1490
|
+
p_cp.set_defaults(func=cmd_copy_doc)
|
|
1491
|
+
|
|
1492
|
+
# ---- edit-content ----
|
|
1493
|
+
p_ec = subparsers.add_parser("edit-content", help="编辑文档正文")
|
|
1494
|
+
p_ec.add_argument("--doc-id", required=True, help="文档 ID(必填)")
|
|
1495
|
+
p_ec.add_argument("--operations-file", required=True, help="JSON 操作文件路径(必填)")
|
|
1496
|
+
p_ec.add_argument("--publish", action="store_true", help="编辑后同步发布")
|
|
1497
|
+
p_ec.set_defaults(func=cmd_edit_content)
|
|
1498
|
+
|
|
1499
|
+
# ---- upload-attachment ----
|
|
1500
|
+
p_ua = subparsers.add_parser("upload-attachment", help="上传附件到文档正文")
|
|
1501
|
+
p_ua.add_argument("--doc-id", required=True, help="文档 ID(必填)")
|
|
1502
|
+
p_ua.add_argument("--file", required=True, help="本地文件路径(必填)")
|
|
1503
|
+
p_ua.add_argument("--no-insert", action="store_true", help="只上传不插入正文")
|
|
1504
|
+
p_ua.add_argument("--view-type", choices=["attachment", "preview"], help="显示方式(默认 attachment)")
|
|
1505
|
+
p_ua.set_defaults(func=cmd_upload_attachment)
|
|
1506
|
+
|
|
1507
|
+
# ---- query-flowchart ----
|
|
1508
|
+
p_qf = subparsers.add_parser("query-flowchart", help="查询文档流程图数据")
|
|
1509
|
+
p_qf.add_argument("--doc-id", required=True, help="文档 ID(必填)")
|
|
1510
|
+
p_qf.add_argument("--flowchart-id", help="流程图 ID(不传则自动从文档正文中发现所有流程图)")
|
|
1511
|
+
p_qf.add_argument("--output", help="输出文件路径(多个流程图时自动添加序号后缀)")
|
|
1512
|
+
p_qf.set_defaults(func=cmd_query_flowchart)
|
|
1513
|
+
|
|
1514
|
+
# ---- add-member ----
|
|
1515
|
+
p_am = subparsers.add_parser("add-member", help="添加文档成员")
|
|
1516
|
+
p_am.add_argument("--doc-id", required=True, help="文档 ID(必填)")
|
|
1517
|
+
p_am.add_argument("--usernames", required=True, help="用户名,多个用逗号分隔(必填)")
|
|
1518
|
+
p_am.add_argument("--role", default="DocReader",
|
|
1519
|
+
choices=["DocReader", "DocEditor", "DocMember", "DocAdmin"],
|
|
1520
|
+
help="角色:DocReader=可读, "
|
|
1521
|
+
"DocEditor/DocMember=可编辑, "
|
|
1522
|
+
"DocAdmin=管理员(默认 DocReader)")
|
|
1523
|
+
p_am.set_defaults(func=cmd_add_member)
|
|
1524
|
+
|
|
1525
|
+
# ---- update-member ----
|
|
1526
|
+
p_um = subparsers.add_parser("update-member", help="更新文档成员角色")
|
|
1527
|
+
p_um.add_argument("--doc-id", required=True, help="文档 ID(必填)")
|
|
1528
|
+
p_um.add_argument("--username", required=True, help="用户名(必填)")
|
|
1529
|
+
p_um.add_argument("--role", required=True,
|
|
1530
|
+
choices=["DocReader", "DocEditor", "DocMember", "DocAdmin"],
|
|
1531
|
+
help="新角色:DocReader=可读, DocEditor/DocMember=可编辑, DocAdmin=管理员")
|
|
1532
|
+
p_um.set_defaults(func=cmd_update_member)
|
|
1533
|
+
|
|
1534
|
+
# ---- change-scope ----
|
|
1535
|
+
p_cs = subparsers.add_parser("change-scope", help="修改文档公开/私密范围")
|
|
1536
|
+
p_cs.add_argument("--doc-id", required=True, help="文档 ID(必填)")
|
|
1537
|
+
p_cs.add_argument("--scope", required=True, type=int, choices=[5, 6, 20],
|
|
1538
|
+
help="范围:5=公开可读, 6=公开可编辑, 20=私密")
|
|
1539
|
+
p_cs.set_defaults(func=cmd_change_scope)
|
|
1540
|
+
|
|
1541
|
+
# ---- query-permission ----
|
|
1542
|
+
p_qp = subparsers.add_parser("query-permission", help="查询用户文档权限")
|
|
1543
|
+
p_qp.add_argument("--doc-id", required=True, help="文档 ID(必填)")
|
|
1544
|
+
p_qp.add_argument("--usernames", required=True, help="用户名,多个用逗号分隔(必填)")
|
|
1545
|
+
p_qp.set_defaults(func=cmd_query_permission)
|
|
1546
|
+
|
|
1547
|
+
# ---- query-comment ----
|
|
1548
|
+
p_qcm = subparsers.add_parser("query-comment", help="查询文档评论")
|
|
1549
|
+
p_qcm.add_argument("--doc-id", required=True, help="文档 ID(必填)")
|
|
1550
|
+
p_qcm.add_argument("--type", default="all", choices=["all", "bottom", "side"],
|
|
1551
|
+
help="评论类型:all=全部(默认), bottom=底部评论, side=划线评论")
|
|
1552
|
+
p_qcm.add_argument("--page", type=int, default=1, help="页码(默认 1)")
|
|
1553
|
+
p_qcm.add_argument("--page-size", type=int, default=100, help="每页条数(默认 100)")
|
|
1554
|
+
p_qcm.add_argument("--raw-json", action="store_true", help="输出原始 JSON")
|
|
1555
|
+
p_qcm.set_defaults(func=cmd_query_comment)
|
|
1556
|
+
|
|
1557
|
+
# ---- query-recent-view ----
|
|
1558
|
+
p_qrv = subparsers.add_parser("query-recent-view", help="查询文档浏览记录")
|
|
1559
|
+
p_qrv.add_argument("--doc-id", required=True, help="文档 ID(必填)")
|
|
1560
|
+
p_qrv.add_argument("--begin-time", help="起始时间(如 2026-03-20、2026-03-20 17:00)")
|
|
1561
|
+
p_qrv.add_argument("--end-time", help="结束时间(如 2026-03-21、2026-03-21 09:00)")
|
|
1562
|
+
p_qrv.add_argument("--page", type=int, default=1, help="页码(默认 1)")
|
|
1563
|
+
p_qrv.add_argument("--page-size", type=int, default=100, help="每页条数(默认 100)")
|
|
1564
|
+
p_qrv.add_argument("--raw-json", action="store_true", help="输出原始 JSON")
|
|
1565
|
+
p_qrv.set_defaults(func=cmd_query_recent_view)
|
|
1566
|
+
|
|
1567
|
+
args = parser.parse_args()
|
|
1568
|
+
if not args.command:
|
|
1569
|
+
parser.print_help()
|
|
1570
|
+
sys.exit(1)
|
|
1571
|
+
args.func(args)
|
|
1572
|
+
|
|
1573
|
+
|
|
1574
|
+
if __name__ == "__main__":
|
|
1575
|
+
main()
|