@comate/zulu 1.1.0 → 1.2.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/README.md +8 -0
- package/comate-engine/assets/skills/auto-commit/SKILL.md +386 -0
- package/comate-engine/assets/skills/auto-commit/references/issue_type_mapping.json +19 -0
- package/comate-engine/assets/skills/auto-commit/references/new_version_instruction.md +196 -0
- package/comate-engine/assets/skills/auto-commit/references/old_version_instruction.md +189 -0
- package/comate-engine/assets/skills/auto-commit/references/query_reference.md +176 -0
- package/comate-engine/assets/skills/auto-commit/scripts/compat.py +86 -0
- package/comate-engine/assets/skills/auto-commit/scripts/create_card_cli.py +67 -0
- package/comate-engine/assets/skills/auto-commit/scripts/git_diff_cli.py +195 -0
- package/comate-engine/assets/skills/auto-commit/scripts/git_utils.py +225 -0
- package/comate-engine/assets/skills/auto-commit/scripts/icafe/__init__.py +66 -0
- package/comate-engine/assets/skills/auto-commit/scripts/icafe/client.py +444 -0
- package/comate-engine/assets/skills/auto-commit/scripts/icafe/farseer.py +53 -0
- package/comate-engine/assets/skills/auto-commit/scripts/icafe/matching.py +778 -0
- package/comate-engine/assets/skills/auto-commit/scripts/logger.py +32 -0
- package/comate-engine/assets/skills/auto-commit/scripts/recognize_card_cli.py +63 -0
- package/comate-engine/assets/skills/automation-browser-comate/SKILL.md +193 -90
- package/comate-engine/assets/skills/figma2code-comate/SKILL.md +2 -2
- package/comate-engine/assets/skills/figma2code-comate/references/codeConnect.md +7 -10
- package/comate-engine/assets/skills/smart-commit/SKILL.md +646 -0
- package/comate-engine/assets/skills/smart-commit/references/issue_type_mapping.json +19 -0
- package/comate-engine/assets/skills/smart-commit/references/query_reference.md +176 -0
- package/comate-engine/assets/skills/smart-commit/scripts/compat.py +86 -0
- package/comate-engine/assets/skills/smart-commit/scripts/create_card_cli.py +67 -0
- package/comate-engine/assets/skills/smart-commit/scripts/git_utils.py +220 -0
- package/comate-engine/assets/skills/smart-commit/scripts/icafe/__init__.py +66 -0
- package/comate-engine/assets/skills/smart-commit/scripts/icafe/client.py +444 -0
- package/comate-engine/assets/skills/smart-commit/scripts/icafe/farseer.py +53 -0
- package/comate-engine/assets/skills/smart-commit/scripts/icafe/matching.py +728 -0
- package/comate-engine/assets/skills/smart-commit/scripts/logger.py +32 -0
- package/comate-engine/assets/skills/smart-commit/scripts/recognize_card_cli.py +63 -0
- package/comate-engine/node_modules/@comate/plugin-engine/dist/index.js +7 -7
- package/comate-engine/node_modules/@comate/plugin-host/dist/index.js +1 -1
- package/comate-engine/node_modules/@comate/plugin-host/dist/main.js +1 -1
- package/comate-engine/node_modules/@comate/plugin-shared-internals/dist/index.js +8 -8
- package/comate-engine/node_modules/@comate/preview-proxy/package.json +2 -2
- package/comate-engine/node_modules/better-sqlite3/build/Release/better_sqlite3.node +0 -0
- package/comate-engine/package.json +2 -2
- package/comate-engine/server.js +61 -44
- package/dist/bundle/index.js +8 -8
- package/package.json +1 -1
- package/comate-engine/node_modules/@comate/plugin-engine/dist/index.d.ts +0 -188
- package/comate-engine/node_modules/@comate/plugin-host/dist/main.d.ts +0 -14
- package/comate-engine/node_modules/@comate/plugin-shared-internals/dist/index.d.ts +0 -4817
- package/comate-engine/node_modules/better-sqlite3/README.md +0 -99
- package/comate-engine/node_modules/bindings/LICENSE.md +0 -22
- package/comate-engine/node_modules/bindings/README.md +0 -98
- package/comate-engine/node_modules/compare-versions/README.md +0 -133
- package/comate-engine/node_modules/compare-versions/lib/esm/compare.d.ts +0 -19
- package/comate-engine/node_modules/compare-versions/lib/esm/compareVersions.d.ts +0 -8
- package/comate-engine/node_modules/compare-versions/lib/esm/index.d.ts +0 -5
- package/comate-engine/node_modules/compare-versions/lib/esm/satisfies.d.ts +0 -14
- package/comate-engine/node_modules/compare-versions/lib/esm/utils.d.ts +0 -7
- package/comate-engine/node_modules/compare-versions/lib/esm/validate.d.ts +0 -28
- package/comate-engine/node_modules/file-uri-to-path/History.md +0 -21
- package/comate-engine/node_modules/file-uri-to-path/README.md +0 -74
- package/comate-engine/node_modules/file-uri-to-path/index.d.ts +0 -2
- package/comate-engine/node_modules/pkce-challenge/README.md +0 -55
- package/comate-engine/node_modules/pkce-challenge/dist/index.browser.d.ts +0 -19
- package/comate-engine/node_modules/pkce-challenge/dist/index.node.d.ts +0 -19
- package/comate-engine/node_modules/sqlite-vec/README.md +0 -1
- package/comate-engine/node_modules/sqlite-vec/index.d.ts +0 -17
- package/comate-engine/node_modules/sqlite-vec-darwin-arm64/README.md +0 -1
- package/comate-engine/node_modules/sqlite-vec-darwin-x64/README.md +0 -1
- package/comate-engine/node_modules/sqlite-vec-linux-arm64/README.md +0 -1
- package/comate-engine/node_modules/sqlite-vec-linux-x64/README.md +0 -1
- package/comate-engine/node_modules/sqlite-vec-windows-x64/README.md +0 -1
- package/comate-engine/node_modules/tree-sitter-bash/README.md +0 -44
- package/comate-engine/node_modules/tree-sitter-bash/bindings/node/binding_test.js +0 -9
- package/comate-engine/node_modules/tree-sitter-bash/bindings/node/index.d.ts +0 -28
- package/comate-engine/node_modules/tree-sitter-bash/bindings/node/index.js +0 -11
- package/comate-engine/node_modules/tree-sitter-bash/prebuilds/darwin-arm64/tree-sitter-bash.node +0 -0
- package/comate-engine/node_modules/tree-sitter-bash/prebuilds/darwin-x64/tree-sitter-bash.node +0 -0
- package/comate-engine/node_modules/tree-sitter-bash/prebuilds/linux-arm64/tree-sitter-bash.node +0 -0
- package/comate-engine/node_modules/tree-sitter-bash/prebuilds/linux-x64/tree-sitter-bash.node +0 -0
- package/comate-engine/node_modules/tree-sitter-bash/prebuilds/win32-arm64/tree-sitter-bash.node +0 -0
- package/comate-engine/node_modules/tree-sitter-bash/prebuilds/win32-x64/tree-sitter-bash.node +0 -0
- package/comate-engine/node_modules/tree-sitter-bash/src/grammar.json +0 -7145
- package/comate-engine/node_modules/tree-sitter-bash/src/node-types.json +0 -2894
- package/comate-engine/node_modules/web-streams-polyfill/README.md +0 -119
- package/comate-engine/node_modules/web-streams-polyfill/types/polyfill.d.ts +0 -28
- package/comate-engine/node_modules/web-streams-polyfill/types/ponyfill.d.ts +0 -809
- package/comate-engine/node_modules/web-tree-sitter/README.md +0 -269
- package/comate-engine/node_modules/web-tree-sitter/debug/tree-sitter.cjs +0 -4558
- package/comate-engine/node_modules/web-tree-sitter/debug/tree-sitter.js +0 -4516
- package/comate-engine/node_modules/web-tree-sitter/debug/tree-sitter.wasm +0 -0
- package/comate-engine/node_modules/web-tree-sitter/web-tree-sitter.d.ts +0 -1030
- package/comate-engine/node_modules/win-ca/README.md +0 -648
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
"""iCafe 自动检测和匹配逻辑
|
|
2
|
+
|
|
3
|
+
包含卡片匹配、自动检测和格式化输出功能。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import datetime
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, List, Any, Optional, Tuple
|
|
12
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
13
|
+
|
|
14
|
+
from .client import ICafeQueryClient, DEFAULT_LOOKBACK_DAYS, DEFAULT_MAX_RECORDS
|
|
15
|
+
from git_utils import get_current_user, extract_space_ids_from_git_log
|
|
16
|
+
from .farseer import get_binding_card_types
|
|
17
|
+
from logger import get_logger
|
|
18
|
+
|
|
19
|
+
logger = get_logger()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def is_smart_commit_enabled() -> bool:
|
|
23
|
+
"""检查绑卡提交功能是否启用
|
|
24
|
+
|
|
25
|
+
读取 ~/.comate/settings.json 中的 enableSmartCommit 字段。
|
|
26
|
+
- 值为 false 时返回 False(功能关闭)
|
|
27
|
+
- 值为 true 或字段不存在时返回 True(默认开启)
|
|
28
|
+
"""
|
|
29
|
+
settings_path = Path.home() / ".comate" / "settings.json"
|
|
30
|
+
try:
|
|
31
|
+
with open(settings_path, "r", encoding="utf-8") as f:
|
|
32
|
+
config = json.load(f)
|
|
33
|
+
return config.get("enableSmartCommit", True) is not False
|
|
34
|
+
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
|
35
|
+
return True # 文件不存在或解析失败,默认开启
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ========================================
|
|
39
|
+
# 自动检测工具函数
|
|
40
|
+
# ========================================
|
|
41
|
+
|
|
42
|
+
def parse_card_id_from_link(link: str) -> Optional[Dict[str, str]]:
|
|
43
|
+
"""从 iCafe 卡片链接或卡片 ID 中解析卡片 ID 和空间前缀
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
link: iCafe 卡片链接或卡片 ID,支持以下格式:
|
|
47
|
+
- 完整链接: "https://console.cloud.baidu-int.com/devops/icafe/issue/DevOps-iScan-35835/show"
|
|
48
|
+
- 卡片 ID: "DevOps-iScan-35835"
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
包含 space_prefix 和 card_id 的字典,解析失败返回 None
|
|
52
|
+
示例:{"space_prefix": "DevOps-iScan", "card_id": "35835"}
|
|
53
|
+
"""
|
|
54
|
+
# 先尝试直接解析卡片 ID 格式(如 "DevOps-iScan-35835")
|
|
55
|
+
card_id_match = re.match(r'^([A-Za-z][A-Za-z0-9_-]*)-(\d+)$', link.strip())
|
|
56
|
+
if card_id_match:
|
|
57
|
+
space_prefix = card_id_match.group(1)
|
|
58
|
+
card_id = card_id_match.group(2)
|
|
59
|
+
return {"space_prefix": space_prefix, "card_id": card_id}
|
|
60
|
+
|
|
61
|
+
# 匹配格式: https://console.cloud.baidu-int.com/devops/icafe/issue/DevOps-iScan-35835/show
|
|
62
|
+
match = re.match(r'.*/issue/([^/]+)(?:/show)?$', link)
|
|
63
|
+
if match:
|
|
64
|
+
full_card_id = match.group(1)
|
|
65
|
+
# 尝试解析为 "SpacePrefix-ID" 格式
|
|
66
|
+
card_match = re.match(r'^(.+)-(\d+)$', full_card_id)
|
|
67
|
+
if card_match:
|
|
68
|
+
space_prefix = card_match.group(1)
|
|
69
|
+
card_id = card_match.group(2)
|
|
70
|
+
return {"space_prefix": space_prefix, "card_id": card_id}
|
|
71
|
+
# 如果不是标准格式,直接返回完整的 ID
|
|
72
|
+
return {"space_prefix": "", "card_id": full_card_id}
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def auto_detect_space(
|
|
77
|
+
client: 'ICafeQueryClient',
|
|
78
|
+
current_user: Optional[str] = None
|
|
79
|
+
) -> Optional[Dict[str, Any]]:
|
|
80
|
+
"""自动检测当前项目对应的 iCafe 空间
|
|
81
|
+
|
|
82
|
+
交叉匹配 git 提交历史中的卡片引用与用户最近访问的空间,
|
|
83
|
+
返回最近提交过且用户有访问记录的空间信息。
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
client: ICafeQueryClient 实例
|
|
87
|
+
current_user: 用户 ID。未提供时自动从 git config 获取。
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
检测到的空间信息字典,包含:
|
|
91
|
+
- prefix_code: 空间前缀(如 "dkx"),用于 IQL 查询
|
|
92
|
+
- space_id: 数字 ID(如 52862),用于 storyRule 等接口
|
|
93
|
+
- name: 空间名称
|
|
94
|
+
检测失败时返回 None
|
|
95
|
+
"""
|
|
96
|
+
# 1. 获取用户 ID
|
|
97
|
+
user = current_user or get_current_user()
|
|
98
|
+
if not user:
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
# 2. 获取最近访问空间
|
|
102
|
+
try:
|
|
103
|
+
api_response = client.get_latest_spaces(user)
|
|
104
|
+
except Exception:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
# API 返回格式: {"status": 200, "message": "OK.", "result": [...]}
|
|
108
|
+
if isinstance(api_response, dict):
|
|
109
|
+
spaces = api_response.get('result', [])
|
|
110
|
+
elif isinstance(api_response, list):
|
|
111
|
+
spaces = api_response
|
|
112
|
+
else:
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
if not spaces:
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
# 构建 prefixCode -> 完整空间信息的映射(小写归一化)
|
|
119
|
+
prefix_to_space = {}
|
|
120
|
+
for space in spaces:
|
|
121
|
+
prefix = space.get('prefixCode', '')
|
|
122
|
+
if prefix:
|
|
123
|
+
prefix_to_space[prefix.lower()] = space
|
|
124
|
+
|
|
125
|
+
# 3. 用已知前缀从 git log 提取空间引用,然后交叉匹配
|
|
126
|
+
known_prefixes = [s.get('prefixCode', '') for s in spaces if s.get('prefixCode')]
|
|
127
|
+
git_space_ids = extract_space_ids_from_git_log(known_prefixes=known_prefixes)
|
|
128
|
+
|
|
129
|
+
if git_space_ids:
|
|
130
|
+
for git_prefix in git_space_ids:
|
|
131
|
+
key = git_prefix.lower()
|
|
132
|
+
if key in prefix_to_space:
|
|
133
|
+
matched = prefix_to_space[key]
|
|
134
|
+
return {
|
|
135
|
+
'prefix_code': matched.get('prefixCode', git_prefix),
|
|
136
|
+
'space_id': matched.get('id'),
|
|
137
|
+
'name': matched.get('name', ''),
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# 4. 回退:git log 无结果或交叉匹配失败,取最近访问的第一个空间
|
|
141
|
+
first = spaces[0]
|
|
142
|
+
return {
|
|
143
|
+
'prefix_code': first.get('prefixCode', ''),
|
|
144
|
+
'space_id': first.get('id'),
|
|
145
|
+
'name': first.get('name', ''),
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _quote_iql_value(value: str) -> str:
|
|
150
|
+
"""对含 IQL 特殊字符的值加双引号,安全值保持原样"""
|
|
151
|
+
if re.search(r'[(),\s"]', value):
|
|
152
|
+
return f'"{value}"'
|
|
153
|
+
return value
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def build_type_filter_iql(type_names: List[str]) -> str:
|
|
157
|
+
"""根据卡片类型名称列表构建 IQL 类型过滤条件
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
type_names: 类型名称列表,如 ["Bug", "Task", "Story"]
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
IQL 类型过滤表达式,如 '类型 in (Bug, Task, Story)'。
|
|
164
|
+
含特殊字符的类型名称会自动加双引号。
|
|
165
|
+
列表为空时返回空字符串。
|
|
166
|
+
"""
|
|
167
|
+
if not type_names:
|
|
168
|
+
return ""
|
|
169
|
+
quoted = [_quote_iql_value(name) for name in type_names]
|
|
170
|
+
if len(quoted) == 1:
|
|
171
|
+
return f"类型 = {quoted[0]}"
|
|
172
|
+
return f"类型 in ({', '.join(quoted)})"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ========================================
|
|
176
|
+
# 空间匹配相关函数
|
|
177
|
+
# ========================================
|
|
178
|
+
|
|
179
|
+
def fetch_space_issue_types(
|
|
180
|
+
client: 'ICafeQueryClient',
|
|
181
|
+
space_prefix: str
|
|
182
|
+
) -> List[Dict[str, Any]]:
|
|
183
|
+
"""获取单个空间的卡片类型列表
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
client: ICafeQueryClient 实例
|
|
187
|
+
space_prefix: 空间前缀
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
格式化的卡片类型列表
|
|
191
|
+
"""
|
|
192
|
+
try:
|
|
193
|
+
raw_response = client.get_space_issue_types(space_prefix)
|
|
194
|
+
formatted_types = []
|
|
195
|
+
issue_types = None
|
|
196
|
+
if isinstance(raw_response, dict):
|
|
197
|
+
if 'result' in raw_response and isinstance(raw_response['result'], dict):
|
|
198
|
+
issue_types = raw_response['result'].get('issueTypes', [])
|
|
199
|
+
elif 'issueTypes' in raw_response:
|
|
200
|
+
issue_types = raw_response['issueTypes']
|
|
201
|
+
elif 'data' in raw_response and isinstance(raw_response['data'], dict):
|
|
202
|
+
issue_types = raw_response['data'].get('issueTypes', [])
|
|
203
|
+
elif isinstance(raw_response, list):
|
|
204
|
+
issue_types = raw_response
|
|
205
|
+
|
|
206
|
+
if issue_types and isinstance(issue_types, list):
|
|
207
|
+
for t in issue_types:
|
|
208
|
+
if isinstance(t, dict):
|
|
209
|
+
type_id = t.get('id', t.get('issueTypeId', ''))
|
|
210
|
+
type_name = t.get('name', t.get('type', ''))
|
|
211
|
+
if type_id or type_name:
|
|
212
|
+
formatted_types.append({
|
|
213
|
+
'id': str(type_id),
|
|
214
|
+
'name': str(type_name)
|
|
215
|
+
})
|
|
216
|
+
return formatted_types
|
|
217
|
+
except Exception:
|
|
218
|
+
return []
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def fetch_all_space_types(
|
|
222
|
+
client: 'ICafeQueryClient',
|
|
223
|
+
space_code_map: Dict[str, Dict[str, Any]]
|
|
224
|
+
) -> Dict[str, List[Dict[str, Any]]]:
|
|
225
|
+
"""并行获取所有空间的卡片类型
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
client: ICafeQueryClient 实例
|
|
229
|
+
space_code_map: 空间前缀 -> 空间信息的映射
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
空间前缀 -> 卡片类型列表的映射
|
|
233
|
+
"""
|
|
234
|
+
space_types_map: Dict[str, List[Dict[str, Any]]] = {}
|
|
235
|
+
with ThreadPoolExecutor(max_workers=5) as pool:
|
|
236
|
+
# 同时提交所有任务并记录对应的前缀
|
|
237
|
+
future_to_prefix = {}
|
|
238
|
+
for prefix_key, space_info in space_code_map.items():
|
|
239
|
+
original_prefix = space_info.get('prefixCode', prefix_key)
|
|
240
|
+
future = pool.submit(fetch_space_issue_types, client, original_prefix)
|
|
241
|
+
future_to_prefix[future] = prefix_key
|
|
242
|
+
|
|
243
|
+
# 收集结果
|
|
244
|
+
for future in future_to_prefix:
|
|
245
|
+
types_list = future.result()
|
|
246
|
+
prefix_key = future_to_prefix[future]
|
|
247
|
+
space_types_map[prefix_key] = types_list
|
|
248
|
+
|
|
249
|
+
return space_types_map
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def resolve_space(
|
|
253
|
+
client: 'ICafeQueryClient',
|
|
254
|
+
user: str,
|
|
255
|
+
space_prefix: Optional[str],
|
|
256
|
+
spaces_future,
|
|
257
|
+
git_log_future
|
|
258
|
+
) -> Tuple[str, Optional[int], str, Dict[str, Dict[str, Any]]]:
|
|
259
|
+
"""解析并匹配空间
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
client: ICafeQueryClient 实例
|
|
263
|
+
user: 用户 ID
|
|
264
|
+
space_prefix: 指定的空间前缀
|
|
265
|
+
spaces_future: 并行获取的空间列表 Future
|
|
266
|
+
git_log_future: 并行获取的 git log Future
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
(space_prefix, space_id_num, space_name, space_code_map)
|
|
270
|
+
"""
|
|
271
|
+
space_id_num: Optional[int] = None
|
|
272
|
+
space_name: str = ""
|
|
273
|
+
space_code_map: Dict[str, Dict[str, Any]] = {}
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
api_response = spaces_future.result()
|
|
277
|
+
spaces_list = (api_response.get('result', [])
|
|
278
|
+
if isinstance(api_response, dict) else api_response)
|
|
279
|
+
for s in (spaces_list or []):
|
|
280
|
+
if isinstance(s, dict):
|
|
281
|
+
code = s.get('prefixCode', '')
|
|
282
|
+
if code:
|
|
283
|
+
space_code_map[code.lower()] = s
|
|
284
|
+
logger.info("获取到 %d 个最近访问空间: %s", len(space_code_map), list(space_code_map.keys()))
|
|
285
|
+
except Exception as e:
|
|
286
|
+
logger.warning("获取空间列表失败: %s", e)
|
|
287
|
+
|
|
288
|
+
# 匹配空间
|
|
289
|
+
if space_prefix:
|
|
290
|
+
matched = space_code_map.get(space_prefix.lower())
|
|
291
|
+
if matched:
|
|
292
|
+
space_id_num = matched.get('id')
|
|
293
|
+
space_name = matched.get('name', '')
|
|
294
|
+
space_prefix = matched.get('prefixCode', space_prefix)
|
|
295
|
+
logger.info("空间精确匹配成功: prefix=%s, id=%s, name=%s", space_prefix, space_id_num, space_name)
|
|
296
|
+
else:
|
|
297
|
+
# 用已知前缀重新跑 git log 精确匹配
|
|
298
|
+
known_prefixes = [s.get('prefixCode', '') for s in space_code_map.values() if s.get('prefixCode')]
|
|
299
|
+
git_space_ids = git_log_future.result()
|
|
300
|
+
|
|
301
|
+
# 如果有已知前缀但 git_log 返回的是旧的正则结果,需要重新精确匹配
|
|
302
|
+
if known_prefixes:
|
|
303
|
+
git_space_ids = extract_space_ids_from_git_log(known_prefixes=known_prefixes)
|
|
304
|
+
|
|
305
|
+
logger.info("git log 提取的前缀: %s", git_space_ids)
|
|
306
|
+
|
|
307
|
+
matched_space = None
|
|
308
|
+
if git_space_ids:
|
|
309
|
+
for git_prefix in git_space_ids:
|
|
310
|
+
if git_prefix.lower() in space_code_map:
|
|
311
|
+
matched_space = space_code_map[git_prefix.lower()]
|
|
312
|
+
logger.info("交叉匹配成功: prefix=%s", git_prefix)
|
|
313
|
+
break
|
|
314
|
+
|
|
315
|
+
if not matched_space and space_code_map:
|
|
316
|
+
matched_space = next(iter(space_code_map.values()))
|
|
317
|
+
logger.warning("交叉匹配无结果,回退到最近访问空间: %s", matched_space.get('prefixCode', ''))
|
|
318
|
+
|
|
319
|
+
if matched_space:
|
|
320
|
+
space_prefix = matched_space.get('prefixCode', '')
|
|
321
|
+
space_id_num = matched_space.get('id')
|
|
322
|
+
space_name = matched_space.get('name', '')
|
|
323
|
+
elif not space_code_map and git_space_ids:
|
|
324
|
+
# space_code_map 为空(API 无返回),使用 git log 最高频前缀兜底
|
|
325
|
+
# 此时没有 space_id_num,但 IQL 查询仍可通过 space_prefix 工作
|
|
326
|
+
space_prefix = git_space_ids[0]
|
|
327
|
+
logger.warning(
|
|
328
|
+
"最近访问空间列表为空,使用 git log 最高频前缀兜底: %s",
|
|
329
|
+
space_prefix,
|
|
330
|
+
)
|
|
331
|
+
# 补全 space_code_map,使 available_spaces 和 space_types_map 不为空
|
|
332
|
+
space_code_map[space_prefix.lower()] = {
|
|
333
|
+
'prefixCode': space_prefix,
|
|
334
|
+
'id': None,
|
|
335
|
+
'name': '',
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if not space_prefix:
|
|
339
|
+
# 收集候选前缀信息,帮助 Agent 快速重试
|
|
340
|
+
candidates = []
|
|
341
|
+
try:
|
|
342
|
+
fallback_ids = git_log_future.result() if not git_space_ids else git_space_ids
|
|
343
|
+
candidates = fallback_ids[:5] if fallback_ids else []
|
|
344
|
+
except Exception:
|
|
345
|
+
pass
|
|
346
|
+
hint = ""
|
|
347
|
+
if candidates:
|
|
348
|
+
hint = f",git log 中发现的候选前缀: {candidates}"
|
|
349
|
+
if not space_code_map:
|
|
350
|
+
hint += "(注意:最近访问空间列表为空,可能是 API 权限问题或用户无最近访问空间)"
|
|
351
|
+
logger.error("无法自动检测空间%s", hint)
|
|
352
|
+
raise ValueError(
|
|
353
|
+
f"无法自动检测空间,请通过 space_prefix 参数指定空间前缀{hint}"
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
return space_prefix, space_id_num, space_name, space_code_map
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def build_type_filter(
|
|
360
|
+
space_id_num: Optional[int],
|
|
361
|
+
include_type_filter: bool
|
|
362
|
+
) -> Tuple[str, List[Dict[str, Any]]]:
|
|
363
|
+
"""构建类型过滤 IQL
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
space_id_num: 空间数字 ID
|
|
367
|
+
include_type_filter: 是否启用类型过滤
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
(type_filter, available_types)
|
|
371
|
+
"""
|
|
372
|
+
type_filter = ""
|
|
373
|
+
available_types = []
|
|
374
|
+
|
|
375
|
+
if include_type_filter and space_id_num is not None:
|
|
376
|
+
type_items = get_binding_card_types(space_id_num)
|
|
377
|
+
available_types = type_items
|
|
378
|
+
type_names = [t['name'] for t in type_items]
|
|
379
|
+
type_filter = build_type_filter_iql(type_names)
|
|
380
|
+
|
|
381
|
+
return type_filter, available_types
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def build_iql_and_query(
|
|
385
|
+
client: 'ICafeQueryClient',
|
|
386
|
+
space_prefix: str,
|
|
387
|
+
space_id_num: Optional[int],
|
|
388
|
+
lookback_days: int,
|
|
389
|
+
type_filter: str,
|
|
390
|
+
max_records: int
|
|
391
|
+
) -> Dict[str, Any]:
|
|
392
|
+
"""构建 IQL 并查询卡片
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
client: ICafeQueryClient 实例
|
|
396
|
+
space_prefix: 空间前缀
|
|
397
|
+
space_id_num: 空间数字 ID
|
|
398
|
+
lookback_days: 回溯天数
|
|
399
|
+
type_filter: 类型过滤 IQL
|
|
400
|
+
max_records: 最大记录数
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
查询结果
|
|
404
|
+
"""
|
|
405
|
+
cutoff_date = (datetime.date.today() - datetime.timedelta(days=lookback_days)).isoformat()
|
|
406
|
+
active_iql = (
|
|
407
|
+
'负责人 = currentUser'
|
|
408
|
+
' AND 流程状态 in (开发中, 进行中, 待开发, 新建, open)'
|
|
409
|
+
f' AND 最后修改时间 > "{cutoff_date}"'
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
if type_filter:
|
|
413
|
+
final_iql = f"{type_filter} AND ({active_iql})"
|
|
414
|
+
else:
|
|
415
|
+
final_iql = active_iql
|
|
416
|
+
|
|
417
|
+
try:
|
|
418
|
+
result = client.query_cards(
|
|
419
|
+
space_id=space_prefix,
|
|
420
|
+
iql=final_iql,
|
|
421
|
+
max_records=max_records,
|
|
422
|
+
is_desc=True,
|
|
423
|
+
order="最后修改时间",
|
|
424
|
+
)
|
|
425
|
+
except Exception as e:
|
|
426
|
+
logger.error("IQL 查询异常: %s", e)
|
|
427
|
+
result = {"cards": []}
|
|
428
|
+
|
|
429
|
+
# 提取简化卡片列表
|
|
430
|
+
raw_cards = result.get("cards", [])
|
|
431
|
+
cards = []
|
|
432
|
+
for card in raw_cards:
|
|
433
|
+
raw_type = card.get("type", "")
|
|
434
|
+
card_type = (raw_type.get("name", str(raw_type))
|
|
435
|
+
if isinstance(raw_type, dict) else str(raw_type))
|
|
436
|
+
raw_status = card.get("status", "")
|
|
437
|
+
card_status = (raw_status.get("name", str(raw_status))
|
|
438
|
+
if isinstance(raw_status, dict) else str(raw_status))
|
|
439
|
+
cards.append({
|
|
440
|
+
"sequence": card.get("sequence", ""),
|
|
441
|
+
"title": card.get("title", ""),
|
|
442
|
+
"type": card_type,
|
|
443
|
+
"status": card_status,
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
return {"cards": cards, "final_iql": final_iql}
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def build_output(
|
|
450
|
+
space_prefix: str,
|
|
451
|
+
space_id_num: Optional[int],
|
|
452
|
+
space_name: str,
|
|
453
|
+
cards: List[Dict],
|
|
454
|
+
final_iql: str,
|
|
455
|
+
available_types: List[Dict],
|
|
456
|
+
space_code_map: Dict[str, Dict[str, Any]],
|
|
457
|
+
space_types_map: Dict[str, List[Dict]],
|
|
458
|
+
) -> Dict[str, Any]:
|
|
459
|
+
"""构建最终输出
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
space_prefix: 空间前缀
|
|
463
|
+
space_id_num: 空间数字 ID
|
|
464
|
+
space_name: 空间名称
|
|
465
|
+
cards: 卡片列表
|
|
466
|
+
final_iql: 最终 IQL
|
|
467
|
+
available_types: 可用类型列表
|
|
468
|
+
space_code_map: 空间映射
|
|
469
|
+
space_types_map: 空间类型映射
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
完整的输出字典
|
|
473
|
+
"""
|
|
474
|
+
# 默认兜底卡片类型
|
|
475
|
+
_fallback_types = [
|
|
476
|
+
{"id": "5007", "name": "Story"},
|
|
477
|
+
{"id": "54444", "name": "Task"},
|
|
478
|
+
{"id": "5009", "name": "Bug"},
|
|
479
|
+
]
|
|
480
|
+
|
|
481
|
+
# 构建可用空间列表(包含每个空间的卡片类型)
|
|
482
|
+
available_spaces = []
|
|
483
|
+
for s in space_code_map.values():
|
|
484
|
+
prefix = s.get('prefixCode', '')
|
|
485
|
+
types = space_types_map.get(prefix.lower(), [])
|
|
486
|
+
available_spaces.append({
|
|
487
|
+
"id": s.get('id'),
|
|
488
|
+
"prefix": prefix,
|
|
489
|
+
"name": s.get('name', ''),
|
|
490
|
+
"types": types if types else _fallback_types
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
# 构建默认值
|
|
494
|
+
defaults = {
|
|
495
|
+
"title": "",
|
|
496
|
+
"type_id": "",
|
|
497
|
+
"space_id": space_id_num
|
|
498
|
+
}
|
|
499
|
+
# 优先使用 farseer 的绑定类型(更精准,仅含绑定了代码提交流程的类型)
|
|
500
|
+
# 兜底使用 space_types_map 的全量类型
|
|
501
|
+
current_space_types = space_types_map.get(space_prefix.lower(), []) if space_prefix else []
|
|
502
|
+
if available_types:
|
|
503
|
+
defaults["type_id"] = available_types[0]["id"]
|
|
504
|
+
elif current_space_types:
|
|
505
|
+
defaults["type_id"] = current_space_types[0]["id"]
|
|
506
|
+
|
|
507
|
+
output: Dict[str, Any] = {
|
|
508
|
+
"cards": cards,
|
|
509
|
+
"space_prefix": space_prefix,
|
|
510
|
+
"space_id": space_id_num,
|
|
511
|
+
"space_name": space_name,
|
|
512
|
+
"final_iql": final_iql,
|
|
513
|
+
"available_types": available_types,
|
|
514
|
+
"available_spaces": available_spaces,
|
|
515
|
+
"defaults": defaults,
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return output
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def build_match_prompt(
|
|
522
|
+
diff_summary: Dict[str, Any],
|
|
523
|
+
cards: List[Dict[str, str]],
|
|
524
|
+
space_prefix: str,
|
|
525
|
+
) -> str:
|
|
526
|
+
"""构建卡片匹配的 LLM 提示词(内部函数)"""
|
|
527
|
+
files_list = "\n".join(f" - {f}" for f in diff_summary["changed_files"])
|
|
528
|
+
|
|
529
|
+
card_lines = []
|
|
530
|
+
for card in cards:
|
|
531
|
+
seq = card["sequence"]
|
|
532
|
+
card_id = f"{space_prefix}-{seq}" if seq else "N/A"
|
|
533
|
+
card_lines.append(
|
|
534
|
+
f" - [{card_id}] ({card['type']}) {card['title']} [状态: {card['status']}]"
|
|
535
|
+
)
|
|
536
|
+
cards_text = "\n".join(card_lines) if card_lines else " (无匹配卡片)"
|
|
537
|
+
|
|
538
|
+
truncation_note = ""
|
|
539
|
+
if diff_summary.get("truncated"):
|
|
540
|
+
truncation_note = "\n注意:diff 内容因过长已被截断,请基于已展示部分进行判断。\n"
|
|
541
|
+
|
|
542
|
+
return f"""请根据以下 git diff 变更内容,对每张候选 iCafe 卡片进行语义匹配度评分,并生成 commit message。
|
|
543
|
+
|
|
544
|
+
## 变更文件
|
|
545
|
+
{files_list}
|
|
546
|
+
|
|
547
|
+
## 变更统计
|
|
548
|
+
{diff_summary["stat_summary"]}
|
|
549
|
+
|
|
550
|
+
## Git Diff 内容
|
|
551
|
+
```
|
|
552
|
+
{diff_summary["diff_content"]}
|
|
553
|
+
```
|
|
554
|
+
{truncation_note}
|
|
555
|
+
## 候选卡片列表
|
|
556
|
+
{cards_text}
|
|
557
|
+
|
|
558
|
+
## 评分要求
|
|
559
|
+
|
|
560
|
+
请对**每张**候选卡片进行匹配度评分(0-100 分),评分维度:
|
|
561
|
+
- **语义相关性**(主要):diff 修改的模块、函数、业务逻辑是否与卡片标题描述的任务相关
|
|
562
|
+
- **类型匹配度**(次要):diff 的变更性质(修复 bug、新增功能、重构等)是否与卡片类型吻合
|
|
563
|
+
- 60 分及以上视为"匹配",低于 60 分视为"不匹配"
|
|
564
|
+
|
|
565
|
+
## 输出格式
|
|
566
|
+
|
|
567
|
+
请严格按以下 JSON 格式输出,不要输出其他内容:
|
|
568
|
+
|
|
569
|
+
```json
|
|
570
|
+
{{
|
|
571
|
+
"ranked_cards": [
|
|
572
|
+
{{"sequence": "<卡片序号>", "score": <0-100>, "reason": "<一句话匹配理由>"}},
|
|
573
|
+
...
|
|
574
|
+
],
|
|
575
|
+
"has_good_match": <true|false>,
|
|
576
|
+
"recommended_commit_message": "<{space_prefix}-<序号> <简要描述>>"
|
|
577
|
+
}}
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
说明:
|
|
581
|
+
- `ranked_cards`:所有候选卡片按 score 降序排列,每张卡片都必须包含
|
|
582
|
+
- `has_good_match`:是否存在 score >= 60 的卡片
|
|
583
|
+
- `recommended_commit_message`:如果有匹配卡片,基于最佳匹配卡片生成;如果无匹配卡片,基于 diff 内容生成(卡片 ID 部分留空为 `{space_prefix}-???`)
|
|
584
|
+
- **低于 60 分的卡片将被过滤,不会展示给用户**"""
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
# ========================================
|
|
588
|
+
# 主入口函数
|
|
589
|
+
# ========================================
|
|
590
|
+
|
|
591
|
+
def find_matching_card(
|
|
592
|
+
client: Optional['ICafeQueryClient'] = None,
|
|
593
|
+
space_prefix: Optional[str] = None,
|
|
594
|
+
current_user: Optional[str] = None,
|
|
595
|
+
max_records: int = DEFAULT_MAX_RECORDS,
|
|
596
|
+
lookback_days: int = DEFAULT_LOOKBACK_DAYS,
|
|
597
|
+
include_type_filter: bool = True,
|
|
598
|
+
) -> Dict[str, Any]:
|
|
599
|
+
"""查询当前用户的活跃 iCafe 卡片
|
|
600
|
+
|
|
601
|
+
封装完整流程:
|
|
602
|
+
1. 并行获取:当前用户 / 最近访问空间 / git log
|
|
603
|
+
2. 交叉匹配确定 iCafe 空间
|
|
604
|
+
3. 构建 IQL 查询卡片
|
|
605
|
+
|
|
606
|
+
注意:git diff 获取已移至 Agent 侧执行,以支持多 workspace 场景。
|
|
607
|
+
脚本仅负责 iCafe 数据获取。
|
|
608
|
+
|
|
609
|
+
Args:
|
|
610
|
+
client: ICafeQueryClient 实例。未提供时自动创建。
|
|
611
|
+
space_prefix: 空间前缀(如 "dkx")。未提供时自动检测。
|
|
612
|
+
current_user: 用户 ID。未提供时自动获取。
|
|
613
|
+
max_records: 查询卡片的最大数量,默认 20。
|
|
614
|
+
lookback_days: 回溯天数,默认 14。
|
|
615
|
+
include_type_filter: 是否调用 farseer 获取卡片类型过滤,默认 True。
|
|
616
|
+
|
|
617
|
+
Returns:
|
|
618
|
+
{
|
|
619
|
+
"cards": [ { sequence, title, type, status }, ... ],
|
|
620
|
+
"space_prefix": "...",
|
|
621
|
+
"space_id": 12345,
|
|
622
|
+
"space_name": "空间名称",
|
|
623
|
+
"final_iql": "...",
|
|
624
|
+
"available_types": [{"id": "5009", "name": "Bug"}, ...],
|
|
625
|
+
"available_spaces": [{"id": 12345, "prefix": "dkx", "name": "空间名称"}, ...],
|
|
626
|
+
"defaults": {
|
|
627
|
+
"title": "", # 由 Agent 根据 diff 生成
|
|
628
|
+
"type_id": "5009", # 默认卡片类型 ID
|
|
629
|
+
"space_id": 12345 # 默认空间 ID
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
Raises:
|
|
634
|
+
ValueError: 无法确定空间或用户时
|
|
635
|
+
"""
|
|
636
|
+
# 前置检查:绑卡提交开关
|
|
637
|
+
if not is_smart_commit_enabled():
|
|
638
|
+
logger.info("绑卡提交功能已关闭,跳过执行")
|
|
639
|
+
return {"disabled": True, "message": "绑卡提交功能已关闭(enableSmartCommit: false)"}
|
|
640
|
+
|
|
641
|
+
logger.info("find_matching_card 开始执行, include_type_filter=%s", include_type_filter)
|
|
642
|
+
|
|
643
|
+
if client is None:
|
|
644
|
+
client: ICafeQueryClient = ICafeQueryClient()
|
|
645
|
+
|
|
646
|
+
# ---- 阶段 1:并行获取本地 git 数据 + API 空间列表 ----
|
|
647
|
+
user = current_user or get_current_user()
|
|
648
|
+
if not user:
|
|
649
|
+
logger.error("无法获取当前用户 ID")
|
|
650
|
+
raise ValueError("无法获取当前用户 ID,请通过 current_user 参数指定")
|
|
651
|
+
|
|
652
|
+
logger.info("提交并行任务: get_latest_spaces(user=%s), extract_git_log", user)
|
|
653
|
+
with ThreadPoolExecutor(max_workers=2) as pool:
|
|
654
|
+
spaces_future = pool.submit(client.get_latest_spaces, user)
|
|
655
|
+
git_log_future = pool.submit(extract_space_ids_from_git_log)
|
|
656
|
+
|
|
657
|
+
# ---- 阶段 2:解析空间列表 + 匹配空间 + 获取各空间卡片类型 ----
|
|
658
|
+
try:
|
|
659
|
+
space_prefix, space_id_num, space_name, space_code_map = resolve_space(
|
|
660
|
+
client, user, space_prefix, spaces_future, git_log_future
|
|
661
|
+
)
|
|
662
|
+
logger.info("空间匹配结果: prefix=%s, id=%s, name=%s", space_prefix, space_id_num, space_name)
|
|
663
|
+
except Exception as e:
|
|
664
|
+
logger.error("空间解析失败: %s", e)
|
|
665
|
+
raise
|
|
666
|
+
|
|
667
|
+
# 并行获取所有空间的卡片类型
|
|
668
|
+
space_types_map = fetch_all_space_types(client, space_code_map)
|
|
669
|
+
logger.info("获取到 %d 个空间的卡片类型", len(space_types_map))
|
|
670
|
+
|
|
671
|
+
# ---- 阶段 3:构建 IQL 并查询卡片 ----
|
|
672
|
+
type_filter, available_types = build_type_filter(space_id_num, include_type_filter)
|
|
673
|
+
|
|
674
|
+
# farseer 获取类型失败时(如 space_id_num 为 None),从 space_types_map 兜底
|
|
675
|
+
if not available_types and space_prefix:
|
|
676
|
+
available_types = space_types_map.get(space_prefix.lower(), [])
|
|
677
|
+
|
|
678
|
+
# 最终兜底:如果仍然为空,使用默认的三种常用卡片类型
|
|
679
|
+
if not available_types:
|
|
680
|
+
available_types = [
|
|
681
|
+
{"id": "5007", "name": "Story"},
|
|
682
|
+
{"id": "5009", "name": "Bug"},
|
|
683
|
+
{"id": "54444", "name": "Task"},
|
|
684
|
+
]
|
|
685
|
+
logger.warning("可绑定卡片类型为空,使用默认兜底类型: Story, Bug, Task")
|
|
686
|
+
|
|
687
|
+
result = build_iql_and_query(client, space_prefix, space_id_num, lookback_days, type_filter, max_records)
|
|
688
|
+
logger.info("IQL 查询完成, 获得 %d 张卡片, iql=%s", len(result["cards"]), result["final_iql"])
|
|
689
|
+
|
|
690
|
+
# ---- 阶段 4:构建输出 ----
|
|
691
|
+
output = build_output(
|
|
692
|
+
space_prefix=space_prefix,
|
|
693
|
+
space_id_num=space_id_num,
|
|
694
|
+
space_name=space_name,
|
|
695
|
+
cards=result["cards"],
|
|
696
|
+
final_iql=result["final_iql"],
|
|
697
|
+
available_types=available_types,
|
|
698
|
+
space_code_map=space_code_map,
|
|
699
|
+
space_types_map=space_types_map,
|
|
700
|
+
)
|
|
701
|
+
logger.info("find_matching_card 执行完成, 返回 %d 张卡片", len(output.get("cards", [])))
|
|
702
|
+
return output
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def match_diff_to_cards(
|
|
706
|
+
client: Optional['ICafeQueryClient'] = None,
|
|
707
|
+
space_prefix: Optional[str] = None,
|
|
708
|
+
current_user: Optional[str] = None,
|
|
709
|
+
max_records: int = DEFAULT_MAX_RECORDS,
|
|
710
|
+
) -> Dict[str, Any]:
|
|
711
|
+
"""向后兼容包装器,委托给 find_matching_card()"""
|
|
712
|
+
return find_matching_card(
|
|
713
|
+
client=client,
|
|
714
|
+
space_prefix=space_prefix,
|
|
715
|
+
current_user=current_user,
|
|
716
|
+
max_records=max_records,
|
|
717
|
+
include_type_filter=True,
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
# ========================================
|
|
722
|
+
# 格式化输出函数
|
|
723
|
+
# ========================================
|
|
724
|
+
|
|
725
|
+
def format_card_summary(card: Dict[str, Any], space_id: str) -> str:
|
|
726
|
+
"""将卡片数据格式化为易读的摘要文本
|
|
727
|
+
|
|
728
|
+
Args:
|
|
729
|
+
card: 卡片数据字典
|
|
730
|
+
space_id: 空间 ID(用于生成链接)
|
|
731
|
+
|
|
732
|
+
Returns:
|
|
733
|
+
格式化的卡片摘要字符串
|
|
734
|
+
"""
|
|
735
|
+
card_id = card.get('sequence') or card.get('id', 'N/A')
|
|
736
|
+
title = card.get('title', '无标题')
|
|
737
|
+
status = card.get('status', 'N/A')
|
|
738
|
+
assignee = card.get('assignee', 'N/A')
|
|
739
|
+
card_type = card.get('type', 'N/A')
|
|
740
|
+
priority = card.get('priority', 'N/A')
|
|
741
|
+
created = card.get('createdAt', card.get('created_at', 'N/A'))
|
|
742
|
+
updated = card.get('updatedAt', card.get('updated_at', 'N/A'))
|
|
743
|
+
|
|
744
|
+
url = f"https://console.cloud.baidu-int.com/devops/icafe/issue/{space_id}-{card_id}/show"
|
|
745
|
+
|
|
746
|
+
lines = [
|
|
747
|
+
f"[{card_id}] {title}",
|
|
748
|
+
f" 类型: {card_type} | 状态: {status} | 优先级: {priority}",
|
|
749
|
+
f" 负责人: {assignee}",
|
|
750
|
+
f" 创建时间: {created} | 更新时间: {updated}",
|
|
751
|
+
f" 链接: {url}",
|
|
752
|
+
]
|
|
753
|
+
return "\n".join(lines)
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def format_query_result(result: Dict[str, Any], space_id: str) -> str:
|
|
757
|
+
"""将查询结果格式化为易读的文本
|
|
758
|
+
|
|
759
|
+
Args:
|
|
760
|
+
result: query_cards 返回的结果字典
|
|
761
|
+
space_id: 空间 ID
|
|
762
|
+
|
|
763
|
+
Returns:
|
|
764
|
+
格式化的查询结果字符串
|
|
765
|
+
"""
|
|
766
|
+
cards = result.get('cards', result.get('data', []))
|
|
767
|
+
total = result.get('total', result.get('totalCount', len(cards)))
|
|
768
|
+
|
|
769
|
+
if not cards:
|
|
770
|
+
return "未查询到符合条件的卡片。"
|
|
771
|
+
|
|
772
|
+
lines = [f"共查询到 {total} 张卡片:\n"]
|
|
773
|
+
for i, card in enumerate(cards, 1):
|
|
774
|
+
lines.append(f"--- 卡片 {i} ---")
|
|
775
|
+
lines.append(format_card_summary(card, space_id))
|
|
776
|
+
lines.append("")
|
|
777
|
+
|
|
778
|
+
return "\n".join(lines)
|