@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,444 @@
|
|
|
1
|
+
"""iCafe API 客户端
|
|
2
|
+
|
|
3
|
+
包含常量、配置类和 API 客户端。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, List, Any, Optional
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
|
|
13
|
+
from logger import get_logger
|
|
14
|
+
|
|
15
|
+
logger = get_logger()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ========================================
|
|
19
|
+
# 常量配置
|
|
20
|
+
# ========================================
|
|
21
|
+
|
|
22
|
+
# iCafe API base URL
|
|
23
|
+
ICAFE_BASE_URL = "http://10.11.152.208:8701/api/process/icafe"
|
|
24
|
+
|
|
25
|
+
# Farseer API base URL
|
|
26
|
+
FARSEER_BASE_URL = "https://farseer.baidu.com/api/v1"
|
|
27
|
+
|
|
28
|
+
# 默认超时时间(秒)
|
|
29
|
+
DEFAULT_TIMEOUT = 30
|
|
30
|
+
|
|
31
|
+
# 默认查询参数
|
|
32
|
+
DEFAULT_LOOKBACK_DAYS = 14
|
|
33
|
+
DEFAULT_MAX_RECORDS = 20
|
|
34
|
+
|
|
35
|
+
# Git log 提取时的最大提交数
|
|
36
|
+
DEFAULT_MAX_COMMITS = 200
|
|
37
|
+
|
|
38
|
+
# Git diff 输出的最大行数
|
|
39
|
+
DEFAULT_MAX_DIFF_LINES = 500
|
|
40
|
+
|
|
41
|
+
# 不是 iCafe 空间 ID 的常见前缀(小写)
|
|
42
|
+
GIT_LOG_FALSE_POSITIVE_PREFIXES = {
|
|
43
|
+
'v', 'sha', 'utf', 'iso', 'rfc', 'p', 'rc', 'pr', 'mr',
|
|
44
|
+
'http', 'https', 'feat', 'fix', 'chore', 'docs', 'style',
|
|
45
|
+
'refactor', 'perf', 'test', 'build', 'ci', 'revert',
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# 卡片类型 ID -> 名称映射表
|
|
49
|
+
ISSUE_TYPE_MAP = {
|
|
50
|
+
56075: "Epic",
|
|
51
|
+
54443: "Feature",
|
|
52
|
+
5007: "Story",
|
|
53
|
+
54444: "Task",
|
|
54
|
+
54621: "Tech Feature",
|
|
55
|
+
54622: "Tech Task",
|
|
56
|
+
5009: "Bug",
|
|
57
|
+
5811: "Bug(线上)",
|
|
58
|
+
5011: "Case",
|
|
59
|
+
65085: "非研发任务",
|
|
60
|
+
49460: "项目",
|
|
61
|
+
5008: "需求",
|
|
62
|
+
5010: "任务",
|
|
63
|
+
88805: "Prompt Story",
|
|
64
|
+
88806: "Prompt Task",
|
|
65
|
+
88807: "GenAI Data Story",
|
|
66
|
+
88808: "GenAI Data Task",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# 反向映射:名称 -> issueTypeId
|
|
70
|
+
ISSUE_TYPE_NAME_MAP = {v: k for k, v in ISSUE_TYPE_MAP.items()}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ========================================
|
|
74
|
+
# 配置类
|
|
75
|
+
# ========================================
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class ICafeQueryConfig:
|
|
79
|
+
"""iCafe 查询 API 配置"""
|
|
80
|
+
|
|
81
|
+
base_url: str = ICAFE_BASE_URL
|
|
82
|
+
timeout: int = DEFAULT_TIMEOUT
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def _get_auth_token() -> Optional[str]:
|
|
86
|
+
"""获取认证 token
|
|
87
|
+
|
|
88
|
+
优先从环境变量 COMATE_AUTH_TOKEN 读取,
|
|
89
|
+
如果没有则从 ~/.comate/login 文件读取
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
认证 token 字符串,如果都未找到则返回 None
|
|
93
|
+
"""
|
|
94
|
+
token = os.environ.get("COMATE_AUTH_TOKEN")
|
|
95
|
+
if token:
|
|
96
|
+
logger.info("Token 来源: env")
|
|
97
|
+
return token
|
|
98
|
+
|
|
99
|
+
login_file = Path.home() / ".comate" / "login"
|
|
100
|
+
if login_file.exists():
|
|
101
|
+
try:
|
|
102
|
+
content = login_file.read_text().strip()
|
|
103
|
+
if content:
|
|
104
|
+
logger.info("Token 来源: file (~/.comate/login)")
|
|
105
|
+
return content
|
|
106
|
+
except Exception:
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
logger.warning("Token 来源: none, 未找到认证 token")
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ========================================
|
|
114
|
+
# API 客户端
|
|
115
|
+
# ========================================
|
|
116
|
+
|
|
117
|
+
class ICafeQueryClient:
|
|
118
|
+
"""iCafe 卡片查询客户端
|
|
119
|
+
|
|
120
|
+
专注于卡片查询场景,支持:
|
|
121
|
+
- 通过 IQL 条件表达式查询卡片
|
|
122
|
+
- 通过卡片 ID 获取卡片详情
|
|
123
|
+
- 查询空间列表和空间计划
|
|
124
|
+
- 分页、排序、展示选项等高级查询能力
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def __init__(self, config: Optional[ICafeQueryConfig] = None):
|
|
128
|
+
self.config = config or ICafeQueryConfig()
|
|
129
|
+
self.session = requests.Session()
|
|
130
|
+
|
|
131
|
+
self.session.headers.update({
|
|
132
|
+
"Content-Type": "application/json"
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
auth_token = self.config._get_auth_token()
|
|
136
|
+
if auth_token:
|
|
137
|
+
self.session.headers["x-ac-Authorization"] = auth_token
|
|
138
|
+
|
|
139
|
+
def _get_headers(self) -> Dict[str, str]:
|
|
140
|
+
"""获取通用请求 headers"""
|
|
141
|
+
return {
|
|
142
|
+
'User-Agent': 'iAPI/1.0.0 (http://iapi.baidu-int.com)'
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
def _get_with_retry(self, url: str, params: dict, max_retries: int = 3) -> requests.Response:
|
|
146
|
+
"""带重试的 GET 请求"""
|
|
147
|
+
import time
|
|
148
|
+
last_exc = None
|
|
149
|
+
for attempt in range(max_retries + 1):
|
|
150
|
+
try:
|
|
151
|
+
response = self.session.get(
|
|
152
|
+
url, params=params, headers=self._get_headers(),
|
|
153
|
+
timeout=self.config.timeout
|
|
154
|
+
)
|
|
155
|
+
response.raise_for_status()
|
|
156
|
+
return response
|
|
157
|
+
except Exception as e:
|
|
158
|
+
last_exc = e
|
|
159
|
+
if attempt < max_retries:
|
|
160
|
+
logger.warning("API 请求失败 (attempt %d/%d): %s, error=%s", attempt + 1, max_retries, url, e)
|
|
161
|
+
time.sleep(0.5 * (attempt + 1))
|
|
162
|
+
logger.error("API 请求最终失败: %s, error=%s", url, last_exc)
|
|
163
|
+
raise last_exc
|
|
164
|
+
|
|
165
|
+
# ========================================
|
|
166
|
+
# 1. 通过 IQL 条件查询卡片
|
|
167
|
+
# ========================================
|
|
168
|
+
def query_cards(self, space_id: str, iql: str,
|
|
169
|
+
page: Optional[int] = None,
|
|
170
|
+
show_detail: bool = False,
|
|
171
|
+
show_associations: bool = False,
|
|
172
|
+
is_desc: bool = False,
|
|
173
|
+
order: Optional[str] = None,
|
|
174
|
+
show_children: bool = False,
|
|
175
|
+
max_records: Optional[int] = None,
|
|
176
|
+
show_okr: bool = False,
|
|
177
|
+
show_accumulate: bool = False) -> Dict[str, Any]:
|
|
178
|
+
"""通过 IQL 条件表达式查询卡片
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
space_id: 空间 ID,如 "joytest"
|
|
182
|
+
iql: IQL 查询表达式,如 "类型 = Bug AND 负责人 = currentUser"
|
|
183
|
+
page: 页码(从 1 开始),可选
|
|
184
|
+
show_detail: 是否显示卡片详情内容,默认 False
|
|
185
|
+
show_associations: 是否显示关联信息,默认 False
|
|
186
|
+
is_desc: 是否降序排列,默认 False
|
|
187
|
+
order: 排序字段(如 "创建时间"、"更新时间"),可选
|
|
188
|
+
show_children: 是否显示子卡片,默认 False
|
|
189
|
+
max_records: 最大返回记录数,可选
|
|
190
|
+
show_okr: 是否显示 OKR 信息,默认 False
|
|
191
|
+
show_accumulate: 是否显示累计信息,默认 False
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
查询结果字典,包含卡片列表和分页信息
|
|
195
|
+
|
|
196
|
+
Raises:
|
|
197
|
+
ValueError: 当 space_id 或 iql 为空时
|
|
198
|
+
requests.RequestException: 请求失败时
|
|
199
|
+
|
|
200
|
+
IQL 表达式示例:
|
|
201
|
+
- "类型 = Bug"
|
|
202
|
+
- "负责人 = currentUser"
|
|
203
|
+
- "负责人 = dongkexin01"
|
|
204
|
+
- "类型 = Bug AND 负责人 = currentUser"
|
|
205
|
+
- "创建时间 > \\"2025-01-01\\""
|
|
206
|
+
- "类型 in (Bug, Epic, Story)"
|
|
207
|
+
- "标题 ~ 测试"
|
|
208
|
+
- "流程状态 in (新建, 开发中)"
|
|
209
|
+
"""
|
|
210
|
+
if not space_id:
|
|
211
|
+
raise ValueError("space_id 不能为空")
|
|
212
|
+
if not iql:
|
|
213
|
+
raise ValueError("iql 查询表达式不能为空")
|
|
214
|
+
|
|
215
|
+
params = {'iql': iql}
|
|
216
|
+
if page is not None:
|
|
217
|
+
params['page'] = page
|
|
218
|
+
if show_detail:
|
|
219
|
+
params['showDetail'] = ''
|
|
220
|
+
if show_associations:
|
|
221
|
+
params['showAssociations'] = ''
|
|
222
|
+
if is_desc:
|
|
223
|
+
params['isDesc'] = ''
|
|
224
|
+
if order:
|
|
225
|
+
params['order'] = order
|
|
226
|
+
if show_children:
|
|
227
|
+
params['showChildren'] = ''
|
|
228
|
+
if max_records:
|
|
229
|
+
params['maxRecords'] = max_records
|
|
230
|
+
if show_okr:
|
|
231
|
+
params['showOkr'] = ''
|
|
232
|
+
if show_accumulate:
|
|
233
|
+
params['showAccumulate'] = ''
|
|
234
|
+
|
|
235
|
+
url = f"{self.config.base_url}/api/spaces/{space_id}/cards"
|
|
236
|
+
logger.info("查询卡片: space=%s, iql=%s", space_id, iql)
|
|
237
|
+
response = self._get_with_retry(url, params)
|
|
238
|
+
result = response.json()
|
|
239
|
+
cards_count = len(result.get("cards", []))
|
|
240
|
+
logger.info("查询卡片返回: %d 张", cards_count)
|
|
241
|
+
return result
|
|
242
|
+
|
|
243
|
+
# ========================================
|
|
244
|
+
# 2. 通过卡片 ID 获取卡片详情
|
|
245
|
+
# ========================================
|
|
246
|
+
def get_card_by_id(self, space_id: str, card_id: str,
|
|
247
|
+
show_associations: bool = False,
|
|
248
|
+
show_children: bool = False,
|
|
249
|
+
show_okr: bool = False,
|
|
250
|
+
show_accumulate: bool = False) -> Dict[str, Any]:
|
|
251
|
+
"""根据空间 ID 和卡片 ID 获取单张卡片详情
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
space_id: 空间 ID
|
|
255
|
+
card_id: 卡片 ID
|
|
256
|
+
show_associations: 是否显示关联信息
|
|
257
|
+
show_children: 是否显示子卡片
|
|
258
|
+
show_okr: 是否显示 OKR 信息
|
|
259
|
+
show_accumulate: 是否显示累计信息
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
卡片信息字典,包含 id, title, description, status, assignee 等
|
|
263
|
+
|
|
264
|
+
Raises:
|
|
265
|
+
ValueError: 当 space_id 或 card_id 为空时
|
|
266
|
+
requests.RequestException: 请求失败时
|
|
267
|
+
"""
|
|
268
|
+
if not space_id or not card_id:
|
|
269
|
+
raise ValueError("space_id 和 card_id 不能为空")
|
|
270
|
+
|
|
271
|
+
params = {}
|
|
272
|
+
if show_associations:
|
|
273
|
+
params['showAssociations'] = ''
|
|
274
|
+
if show_children:
|
|
275
|
+
params['showChildren'] = ''
|
|
276
|
+
if show_okr:
|
|
277
|
+
params['showOkr'] = ''
|
|
278
|
+
if show_accumulate:
|
|
279
|
+
params['showAccumulate'] = ''
|
|
280
|
+
|
|
281
|
+
url = f"{self.config.base_url}/api/spaces/{space_id}/cards/{card_id}"
|
|
282
|
+
response = self._get_with_retry(url, params)
|
|
283
|
+
return response.json()
|
|
284
|
+
|
|
285
|
+
# ========================================
|
|
286
|
+
# 3. 查询最近访问的空间列表
|
|
287
|
+
# ========================================
|
|
288
|
+
def get_latest_spaces(self, current_user: str, limit: int = 3) -> List[Dict[str, Any]]:
|
|
289
|
+
"""查询用户最近访问的空间列表
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
current_user: 用户 ID,如 "dongkexin01"
|
|
293
|
+
limit: 最多返回的空间数量,默认 3
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
空间列表,每个空间包含 id, name, prefix_code, access_time 等
|
|
297
|
+
|
|
298
|
+
Raises:
|
|
299
|
+
ValueError: 当 current_user 为空时
|
|
300
|
+
requests.RequestException: 请求失败时
|
|
301
|
+
"""
|
|
302
|
+
if not current_user:
|
|
303
|
+
raise ValueError("current_user 不能为空")
|
|
304
|
+
|
|
305
|
+
logger.info("查询最近访问空间: user=%s, limit=%d", current_user, limit)
|
|
306
|
+
url = f"{self.config.base_url}/api/v2/space/latest"
|
|
307
|
+
params = {'currentUser': current_user}
|
|
308
|
+
response = self._get_with_retry(url, params)
|
|
309
|
+
data = response.json()
|
|
310
|
+
|
|
311
|
+
# 对返回的空间列表截取前 limit 个
|
|
312
|
+
if isinstance(data, dict) and 'result' in data:
|
|
313
|
+
data['result'] = data['result'][:limit]
|
|
314
|
+
elif isinstance(data, list):
|
|
315
|
+
data = data[:limit]
|
|
316
|
+
|
|
317
|
+
return data
|
|
318
|
+
|
|
319
|
+
# ========================================
|
|
320
|
+
# 4. 获取空间内所有计划
|
|
321
|
+
# ========================================
|
|
322
|
+
def get_space_plans(self, space_id: str) -> List[Dict[str, Any]]:
|
|
323
|
+
"""获取空间内所有计划
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
space_id: 空间 ID
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
计划列表,每个计划包含 id, name, start_date, end_date, status 等
|
|
330
|
+
|
|
331
|
+
Raises:
|
|
332
|
+
ValueError: 当 space_id 为空时
|
|
333
|
+
requests.RequestException: 请求失败时
|
|
334
|
+
"""
|
|
335
|
+
if not space_id:
|
|
336
|
+
raise ValueError("space_id 不能为空")
|
|
337
|
+
|
|
338
|
+
url = f"{self.config.base_url}/api/v2/space/{space_id}/plans"
|
|
339
|
+
response = self._get_with_retry(url, {})
|
|
340
|
+
return response.json()
|
|
341
|
+
|
|
342
|
+
# ========================================
|
|
343
|
+
# 5. 获取空间卡片类型列表
|
|
344
|
+
# ========================================
|
|
345
|
+
def get_space_issue_types(self, space_prefix: str) -> List[Dict[str, Any]]:
|
|
346
|
+
"""获取空间的卡片类型列表
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
space_prefix: 空间前缀(如 "dkx")
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
卡片类型列表,每个类型包含 id, name 等
|
|
353
|
+
|
|
354
|
+
Raises:
|
|
355
|
+
ValueError: 当 space_prefix 为空时
|
|
356
|
+
requests.RequestException: 请求失败时
|
|
357
|
+
"""
|
|
358
|
+
if not space_prefix:
|
|
359
|
+
raise ValueError("space_prefix 不能为空")
|
|
360
|
+
|
|
361
|
+
url = f"{self.config.base_url}/api/v2/space/{space_prefix}/issueTypes"
|
|
362
|
+
response = self._get_with_retry(url, {})
|
|
363
|
+
return response.json()
|
|
364
|
+
|
|
365
|
+
# ========================================
|
|
366
|
+
# 6. 创建卡片
|
|
367
|
+
# ========================================
|
|
368
|
+
def create_card(
|
|
369
|
+
self,
|
|
370
|
+
title: str,
|
|
371
|
+
space_id: str,
|
|
372
|
+
card_type: str,
|
|
373
|
+
description: str = "",
|
|
374
|
+
assignee_id: Optional[str] = None,
|
|
375
|
+
) -> Optional[Dict[str, Any]]:
|
|
376
|
+
"""创建 iCafe 卡片
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
title: 卡片标题
|
|
380
|
+
space_id: 空间前缀(字符串类型,如 "dkx"),注意不是数字 ID
|
|
381
|
+
card_type: 卡片类型名称(如 "Bug", "Story")
|
|
382
|
+
description: 卡片描述,默认为空
|
|
383
|
+
assignee_id: 负责人 ID,默认为当前用户
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
创建成功的卡片信息,包含:id, title, type, sequence 等。
|
|
387
|
+
失败时返回 None。
|
|
388
|
+
|
|
389
|
+
Note:
|
|
390
|
+
此实现完全参考 icafe-card-assistant 的 icafe_client.py create_card 方法
|
|
391
|
+
重要: space_id 参数应该是空间前缀(如 "dkx"),而不是数字 ID
|
|
392
|
+
"""
|
|
393
|
+
try:
|
|
394
|
+
if not title or not space_id:
|
|
395
|
+
return None
|
|
396
|
+
|
|
397
|
+
logger.info("创建卡片: title=%s, space=%s, type=%s, assignee=%s", title, space_id, card_type, assignee_id)
|
|
398
|
+
issue = {
|
|
399
|
+
'title': title,
|
|
400
|
+
'detail': description, # 简化处理,不做 HTML 转换
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if card_type:
|
|
404
|
+
issue['type'] = card_type
|
|
405
|
+
|
|
406
|
+
# 构建自定义字段
|
|
407
|
+
fields = {}
|
|
408
|
+
if assignee_id:
|
|
409
|
+
fields['负责人'] = assignee_id
|
|
410
|
+
|
|
411
|
+
if fields:
|
|
412
|
+
issue['fields'] = fields
|
|
413
|
+
|
|
414
|
+
data = {
|
|
415
|
+
'issues': [issue]
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
url = f"{self.config.base_url}/api/v2/space/{space_id}/issue/new"
|
|
419
|
+
headers = {
|
|
420
|
+
'User-Agent': 'iAPI/1.0.0 (http://iapi.baidu-int.com)',
|
|
421
|
+
'Content-Type': 'application/json'
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
# 使用 self.session.post 发送请求
|
|
425
|
+
response = self.session.post(url, json=data, headers=headers, timeout=self.config.timeout)
|
|
426
|
+
response.raise_for_status()
|
|
427
|
+
result = response.json()
|
|
428
|
+
|
|
429
|
+
# 检查返回的状态
|
|
430
|
+
if isinstance(result, dict):
|
|
431
|
+
response_status = result.get('status')
|
|
432
|
+
if response_status != 200:
|
|
433
|
+
logger.error("创建卡片失败: API 返回业务状态码 %s, 消息: %s", response_status, result.get('message', 'Unknown'))
|
|
434
|
+
print(f"错误: API 返回业务状态码 {response_status}, 消息: {result.get('message', 'Unknown')}", file=__import__('sys').stderr)
|
|
435
|
+
return None
|
|
436
|
+
|
|
437
|
+
issues = result.get("issues", [])
|
|
438
|
+
seq = issues[0].get("sequence") if issues else "unknown"
|
|
439
|
+
logger.info("创建卡片成功: sequence=%s", seq)
|
|
440
|
+
return result
|
|
441
|
+
except Exception as e:
|
|
442
|
+
logger.error("创建卡片异常: %s: %s", type(e).__name__, e)
|
|
443
|
+
print(f"创建卡片异常: {type(e).__name__}: {e}", file=__import__('sys').stderr)
|
|
444
|
+
return None
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Farseer API 客户端"""
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
from typing import Dict, List, Any, Optional
|
|
5
|
+
|
|
6
|
+
from .client import FARSEER_BASE_URL, DEFAULT_TIMEOUT, ISSUE_TYPE_MAP
|
|
7
|
+
from logger import get_logger
|
|
8
|
+
|
|
9
|
+
logger = get_logger()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_binding_card_types(space_id: int, timeout: int = DEFAULT_TIMEOUT) -> List[Dict[str, Any]]:
|
|
13
|
+
"""查询空间可绑定的卡片类型列表(含 ID 和名称)
|
|
14
|
+
|
|
15
|
+
调用 farseer storyRule 接口获取空间的 engineeringBindingTypes,
|
|
16
|
+
返回包含 issueTypeId 和类型名称的完整列表。
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
space_id: 空间数字 ID(如 56355)
|
|
20
|
+
timeout: 请求超时时间(秒)
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
卡片类型列表,如 [{"id": "5009", "name": "Bug"}, {"id": "5007", "name": "Story"}]。
|
|
24
|
+
接口调用失败或无配置时返回空列表。
|
|
25
|
+
"""
|
|
26
|
+
logger.info("查询 farseer 可绑定类型: space_id=%s", space_id)
|
|
27
|
+
try:
|
|
28
|
+
url = f"{FARSEER_BASE_URL}/storyRule/getRule"
|
|
29
|
+
response = requests.get(
|
|
30
|
+
url,
|
|
31
|
+
params={'icafeSpaceId': space_id},
|
|
32
|
+
timeout=timeout
|
|
33
|
+
)
|
|
34
|
+
response.raise_for_status()
|
|
35
|
+
data = response.json()
|
|
36
|
+
|
|
37
|
+
rule_data = data.get('data', {}).get('data', {})
|
|
38
|
+
binding_types = rule_data.get('engineeringBindingTypes', [])
|
|
39
|
+
|
|
40
|
+
result = []
|
|
41
|
+
for item in binding_types:
|
|
42
|
+
type_id = item.get('issueTypeId')
|
|
43
|
+
if type_id and type_id in ISSUE_TYPE_MAP:
|
|
44
|
+
result.append({
|
|
45
|
+
'id': str(type_id),
|
|
46
|
+
'name': ISSUE_TYPE_MAP[type_id]
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
logger.info("获取到 %d 种可绑定卡片类型", len(result))
|
|
50
|
+
return result
|
|
51
|
+
except Exception as e:
|
|
52
|
+
logger.warning("farseer 查询失败: %s", e)
|
|
53
|
+
return []
|