@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.
Files changed (88) hide show
  1. package/README.md +8 -0
  2. package/comate-engine/assets/skills/auto-commit/SKILL.md +386 -0
  3. package/comate-engine/assets/skills/auto-commit/references/issue_type_mapping.json +19 -0
  4. package/comate-engine/assets/skills/auto-commit/references/new_version_instruction.md +196 -0
  5. package/comate-engine/assets/skills/auto-commit/references/old_version_instruction.md +189 -0
  6. package/comate-engine/assets/skills/auto-commit/references/query_reference.md +176 -0
  7. package/comate-engine/assets/skills/auto-commit/scripts/compat.py +86 -0
  8. package/comate-engine/assets/skills/auto-commit/scripts/create_card_cli.py +67 -0
  9. package/comate-engine/assets/skills/auto-commit/scripts/git_diff_cli.py +195 -0
  10. package/comate-engine/assets/skills/auto-commit/scripts/git_utils.py +225 -0
  11. package/comate-engine/assets/skills/auto-commit/scripts/icafe/__init__.py +66 -0
  12. package/comate-engine/assets/skills/auto-commit/scripts/icafe/client.py +444 -0
  13. package/comate-engine/assets/skills/auto-commit/scripts/icafe/farseer.py +53 -0
  14. package/comate-engine/assets/skills/auto-commit/scripts/icafe/matching.py +778 -0
  15. package/comate-engine/assets/skills/auto-commit/scripts/logger.py +32 -0
  16. package/comate-engine/assets/skills/auto-commit/scripts/recognize_card_cli.py +63 -0
  17. package/comate-engine/assets/skills/automation-browser-comate/SKILL.md +193 -90
  18. package/comate-engine/assets/skills/figma2code-comate/SKILL.md +2 -2
  19. package/comate-engine/assets/skills/figma2code-comate/references/codeConnect.md +7 -10
  20. package/comate-engine/assets/skills/smart-commit/SKILL.md +646 -0
  21. package/comate-engine/assets/skills/smart-commit/references/issue_type_mapping.json +19 -0
  22. package/comate-engine/assets/skills/smart-commit/references/query_reference.md +176 -0
  23. package/comate-engine/assets/skills/smart-commit/scripts/compat.py +86 -0
  24. package/comate-engine/assets/skills/smart-commit/scripts/create_card_cli.py +67 -0
  25. package/comate-engine/assets/skills/smart-commit/scripts/git_utils.py +220 -0
  26. package/comate-engine/assets/skills/smart-commit/scripts/icafe/__init__.py +66 -0
  27. package/comate-engine/assets/skills/smart-commit/scripts/icafe/client.py +444 -0
  28. package/comate-engine/assets/skills/smart-commit/scripts/icafe/farseer.py +53 -0
  29. package/comate-engine/assets/skills/smart-commit/scripts/icafe/matching.py +728 -0
  30. package/comate-engine/assets/skills/smart-commit/scripts/logger.py +32 -0
  31. package/comate-engine/assets/skills/smart-commit/scripts/recognize_card_cli.py +63 -0
  32. package/comate-engine/node_modules/@comate/plugin-engine/dist/index.js +7 -7
  33. package/comate-engine/node_modules/@comate/plugin-host/dist/index.js +1 -1
  34. package/comate-engine/node_modules/@comate/plugin-host/dist/main.js +1 -1
  35. package/comate-engine/node_modules/@comate/plugin-shared-internals/dist/index.js +8 -8
  36. package/comate-engine/node_modules/@comate/preview-proxy/package.json +2 -2
  37. package/comate-engine/node_modules/better-sqlite3/build/Release/better_sqlite3.node +0 -0
  38. package/comate-engine/package.json +2 -2
  39. package/comate-engine/server.js +61 -44
  40. package/dist/bundle/index.js +8 -8
  41. package/package.json +1 -1
  42. package/comate-engine/node_modules/@comate/plugin-engine/dist/index.d.ts +0 -188
  43. package/comate-engine/node_modules/@comate/plugin-host/dist/main.d.ts +0 -14
  44. package/comate-engine/node_modules/@comate/plugin-shared-internals/dist/index.d.ts +0 -4817
  45. package/comate-engine/node_modules/better-sqlite3/README.md +0 -99
  46. package/comate-engine/node_modules/bindings/LICENSE.md +0 -22
  47. package/comate-engine/node_modules/bindings/README.md +0 -98
  48. package/comate-engine/node_modules/compare-versions/README.md +0 -133
  49. package/comate-engine/node_modules/compare-versions/lib/esm/compare.d.ts +0 -19
  50. package/comate-engine/node_modules/compare-versions/lib/esm/compareVersions.d.ts +0 -8
  51. package/comate-engine/node_modules/compare-versions/lib/esm/index.d.ts +0 -5
  52. package/comate-engine/node_modules/compare-versions/lib/esm/satisfies.d.ts +0 -14
  53. package/comate-engine/node_modules/compare-versions/lib/esm/utils.d.ts +0 -7
  54. package/comate-engine/node_modules/compare-versions/lib/esm/validate.d.ts +0 -28
  55. package/comate-engine/node_modules/file-uri-to-path/History.md +0 -21
  56. package/comate-engine/node_modules/file-uri-to-path/README.md +0 -74
  57. package/comate-engine/node_modules/file-uri-to-path/index.d.ts +0 -2
  58. package/comate-engine/node_modules/pkce-challenge/README.md +0 -55
  59. package/comate-engine/node_modules/pkce-challenge/dist/index.browser.d.ts +0 -19
  60. package/comate-engine/node_modules/pkce-challenge/dist/index.node.d.ts +0 -19
  61. package/comate-engine/node_modules/sqlite-vec/README.md +0 -1
  62. package/comate-engine/node_modules/sqlite-vec/index.d.ts +0 -17
  63. package/comate-engine/node_modules/sqlite-vec-darwin-arm64/README.md +0 -1
  64. package/comate-engine/node_modules/sqlite-vec-darwin-x64/README.md +0 -1
  65. package/comate-engine/node_modules/sqlite-vec-linux-arm64/README.md +0 -1
  66. package/comate-engine/node_modules/sqlite-vec-linux-x64/README.md +0 -1
  67. package/comate-engine/node_modules/sqlite-vec-windows-x64/README.md +0 -1
  68. package/comate-engine/node_modules/tree-sitter-bash/README.md +0 -44
  69. package/comate-engine/node_modules/tree-sitter-bash/bindings/node/binding_test.js +0 -9
  70. package/comate-engine/node_modules/tree-sitter-bash/bindings/node/index.d.ts +0 -28
  71. package/comate-engine/node_modules/tree-sitter-bash/bindings/node/index.js +0 -11
  72. package/comate-engine/node_modules/tree-sitter-bash/prebuilds/darwin-arm64/tree-sitter-bash.node +0 -0
  73. package/comate-engine/node_modules/tree-sitter-bash/prebuilds/darwin-x64/tree-sitter-bash.node +0 -0
  74. package/comate-engine/node_modules/tree-sitter-bash/prebuilds/linux-arm64/tree-sitter-bash.node +0 -0
  75. package/comate-engine/node_modules/tree-sitter-bash/prebuilds/linux-x64/tree-sitter-bash.node +0 -0
  76. package/comate-engine/node_modules/tree-sitter-bash/prebuilds/win32-arm64/tree-sitter-bash.node +0 -0
  77. package/comate-engine/node_modules/tree-sitter-bash/prebuilds/win32-x64/tree-sitter-bash.node +0 -0
  78. package/comate-engine/node_modules/tree-sitter-bash/src/grammar.json +0 -7145
  79. package/comate-engine/node_modules/tree-sitter-bash/src/node-types.json +0 -2894
  80. package/comate-engine/node_modules/web-streams-polyfill/README.md +0 -119
  81. package/comate-engine/node_modules/web-streams-polyfill/types/polyfill.d.ts +0 -28
  82. package/comate-engine/node_modules/web-streams-polyfill/types/ponyfill.d.ts +0 -809
  83. package/comate-engine/node_modules/web-tree-sitter/README.md +0 -269
  84. package/comate-engine/node_modules/web-tree-sitter/debug/tree-sitter.cjs +0 -4558
  85. package/comate-engine/node_modules/web-tree-sitter/debug/tree-sitter.js +0 -4516
  86. package/comate-engine/node_modules/web-tree-sitter/debug/tree-sitter.wasm +0 -0
  87. package/comate-engine/node_modules/web-tree-sitter/web-tree-sitter.d.ts +0 -1030
  88. 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 []