@comate/zulu 1.2.0 → 1.2.1-beta.1

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.
@@ -30,23 +30,9 @@ metadata:
30
30
  - "当前版本不支持..."
31
31
  - 任何提及"版本"、"降级"、"兼容"、"开关"等字眼
32
32
 
33
- ### 0a. 开关检查(本地用户偏好)
33
+ ### 0a. 版本兼容性检查(纯文本判断,无需命令)
34
34
 
35
- **说明:此检查读取用户本地配置,是用户个人的开关偏好。步骤 1 中的 `result.disabled` 是服务端/空间级别的开关,两者独立,互不替代。**
36
-
37
- 通过以下方式进行检查(**必须加 `__silent:` 前缀**):
38
-
39
- ```
40
- __silent: cat ~/.comate/settings.json 2>/dev/null || echo '{}'
41
- ```
42
-
43
- 解析返回的 JSON,检查 `enableSmartCommit` 字段:
44
- - 如果 `enableSmartCommit` 为 `false` → **立即结束 skill,不再输出任何内容,不再执行任何后续步骤**
45
- - 如果 `enableSmartCommit` 为 `true` 或字段不存在(默认开启)→ **继续执行 0b**
46
-
47
- ### 0b. 版本兼容性检查
48
-
49
- **⚠️ 必须在 0a 完成后执行此步骤,不可跳过。**
35
+ **⚠️ 此步骤必须最先执行。它决定后续命令是否需要加 `__silent:` 前缀。**
50
36
 
51
37
  通过检查上方「版本信息」中的占位符是否被替换来判断:
52
38
 
@@ -60,6 +46,60 @@ __silent: cat ~/.comate/settings.json 2>/dev/null || echo '{}'
60
46
  - **新版本** → 请参考 `references/new_version_instruction.md`
61
47
  - **旧版本** → 请参考 `references/old_version_instruction.md`
62
48
 
49
+ **记住判断结果,后续步骤中:**
50
+ - **新版本**:命令加 `__silent:` 前缀
51
+ - **旧版本**:命令不加 `__silent:` 前缀
52
+
53
+ **继续执行 0b。**
54
+
55
+ ### 0b. 开关检查 + Python 检测(并行执行)
56
+
57
+ **⚠️ 必须在 0a 完成后执行此步骤,不可跳过。两条命令无依赖,Agent 必须在同一次响应中并行发出。**
58
+
59
+ **说明:开关检查读取用户本地配置(`~/.comate/settings.json` 中的 `enableSmartCommit`),是用户个人的开关偏好。步骤 1 中的 `result.disabled` 是服务端/空间级别的开关,两者独立,互不替代。**
60
+
61
+ #### 命令 1:开关检查(纯 shell,无外部依赖)
62
+
63
+ Agent 根据环境上下文中的操作系统信息选择对应命令(**新版本加 `__silent:` 前缀,旧版本不加**):
64
+
65
+ **macOS / Linux:**
66
+ ```
67
+ [__silent: ]cat ~/.comate/settings.json 2>/dev/null || echo "{}"
68
+ ```
69
+
70
+ **Windows(cmd):**
71
+ ```
72
+ [__silent: ]type "%USERPROFILE%\.comate\settings.json" 2>NUL || echo {}
73
+ ```
74
+
75
+ Agent 解析输出的 JSON,检查 `enableSmartCommit` 字段:
76
+ - 如果 `enableSmartCommit` 为 `false` → **立即结束 skill,不再输出任何内容,不再执行任何后续步骤**
77
+ - 如果 `enableSmartCommit` 为 `true`、字段不存在、或命令输出为 `{}`(文件不存在)→ 默认开启,继续
78
+
79
+ #### 命令 2:Python 可用性检测
80
+
81
+ 与命令 1 **并行**发出(**新版本加 `__silent:` 前缀,旧版本不加**):
82
+
83
+ **macOS / Linux:**
84
+ ```
85
+ [__silent: ]python3 --version 2>/dev/null || python --version 2>/dev/null
86
+ ```
87
+
88
+ **Windows(cmd):**
89
+ ```
90
+ [__silent: ]python --version 2>NUL || python3 --version 2>NUL || py -3 --version 2>NUL
91
+ ```
92
+
93
+ - 如果命令**成功**(返回版本号,如 `Python 3.x.x`)→ 记住**输出中实际成功的那个命令名**(`python3`、`python` 或 `py -3`),后续步骤中所有 `python3` 必须替换为该命令名
94
+ - 如果**全部失败** → 标记 `python_available = false`。后续步骤 1 直接进入"iCafe 获取失败降级"流程,步骤 2 中 `git_diff_cli.py` 也无法执行,Agent 应直接执行 git 命令获取 diff 信息
95
+
96
+ #### 跨平台说明
97
+
98
+ Agent 根据环境上下文的操作系统信息判断当前平台。同时需注意:
99
+ - **macOS / Linux**:shell 命令中使用 `2>/dev/null` 抑制错误
100
+ - **Windows(cmd)**:使用 `2>NUL` 抑制错误,路径使用 `%USERPROFILE%`,`cd` 跨盘符需用 `cd /d`
101
+ - 后续步骤中所有 `cd <path> && <command>` 在 Windows 上应改为 `cd /d <path> && <command>`
102
+
63
103
  ---
64
104
 
65
105
  ## 步骤 1:获取 iCafe 数据
@@ -67,15 +107,11 @@ __silent: cat ~/.comate/settings.json 2>/dev/null || echo '{}'
67
107
  通过 `run_command` 执行以下命令获取 iCafe 卡片数据。**新版本加 `__silent:` 前缀,旧版本不加。**
68
108
 
69
109
  ```bash
70
- [__silent: ]cd <用户项目目录> && PYTHONPATH="<skill_directory>/scripts" python3 -c "
71
- import sys, json
72
- sys.path.insert(0, '<skill_directory>/scripts')
73
- from icafe.matching import find_matching_card
74
- result = find_matching_card(current_user=sys.argv[1] if len(sys.argv) > 1 else None)
75
- print(json.dumps(result))
76
- " '${COMATE_USERNAME}'
110
+ [__silent: ]python3 <skill_directory>/scripts/match_card_cli.py --username "${COMATE_USERNAME}"
77
111
  ```
78
112
 
113
+ 脚本内部通过 `os.environ.get("COMATE_USERNAME")` 读取用户名(跨平台兼容),`${COMATE_USERNAME}` 由 Agent 构建命令时替换为实际值。
114
+
79
115
  从 stdout 输出的 JSON 解析为 `result` 对象。
80
116
 
81
117
  **⚠️ 开关检查(服务端/空间级别):** 解析 `result` 后,**首先**检查 `result.get("disabled")` 是否为 `True`。如果是,**必须立即结束 skill,不再执行任何后续步骤,不输出任何内容**。此开关与步骤 0a 的本地用户偏好开关独立,由服务端控制。
@@ -334,7 +370,7 @@ Agent 内部分析以下信号判断是否展示提交确认,**不输出任何
334
370
  | space_name | 空间名称 |
335
371
  | final_iql | 实际执行的完整 IQL |
336
372
  | available_types | 可绑定卡片类型列表:[{id, name}, ...] |
337
- | available_spaces | 可访问空间列表:[{id, prefix, name}, ...] |
373
+ | available_spaces | 可访问空间列表:[{id, prefix, name, types}, ...] |
338
374
  | defaults | 默认表单值:{title, type_id, space_id}(title 由 Agent 在步骤 2 中生成) |
339
375
 
340
376
  ### `diff_summary`(Agent 在步骤 2 中构建)
@@ -368,6 +404,19 @@ token 按优先级读取:
368
404
  1. 环境变量 `COMATE_AUTH_TOKEN`
369
405
  2. 文件 `~/.comate/login`
370
406
 
407
+ ### 跨平台 Python 命令
408
+
409
+ 本 skill 中所有 `python3` 命令在 Windows 上可能不可用。步骤 0a 会自动检测可用的 Python 命令名,后续步骤中的 `python3` 必须替换为检测到的命令名:
410
+ - **macOS / Linux**:通常为 `python3`
411
+ - **Windows**:通常为 `python` 或 `py -3`
412
+
413
+ ### 跨平台 Shell 命令
414
+
415
+ 本 skill 中涉及 `cd <path> && <command>` 的命令模板在 Windows 上需注意:
416
+ - **macOS / Linux**:`cd /path/to/project && git add .`
417
+ - **Windows(cmd)**:需使用 `cd /d <path>` 以支持跨盘符切换,如 `cd /d D:\project && git add .`
418
+ - Agent 执行命令时应根据环境上下文中的操作系统信息自动选择正确的 `cd` 写法。
419
+
371
420
  ### 资源
372
421
 
373
422
  - **scripts/icafe/** — 核心实现包
@@ -376,6 +425,7 @@ token 按优先级读取:
376
425
  - `farseer.py` — `get_binding_card_types()` 获取可绑定卡片类型
377
426
  - **scripts/git_utils.py** — diff 摘要、用户获取、git log 提取
378
427
  - **scripts/git_diff_cli.py** — git diff 摘要命令行工具(步骤 2)
428
+ - **scripts/match_card_cli.py** — 查询匹配卡片命令行工具(步骤 1)
379
429
  - **scripts/create_card_cli.py** — 创建卡片命令行工具
380
430
  - **scripts/recognize_card_cli.py** — 识别卡片命令行工具
381
431
  - **scripts/compat.py** — 版本兼容性检测模块
@@ -77,7 +77,7 @@ command = f"__interactive:icafe-cards {payload}"
77
77
 
78
78
  完整示例 command 值:
79
79
  ```
80
- __interactive:icafe-cards {"cards":[{"sequence":"200","title":"修复登录问题","type":"Bug","status":"开发中"}],"spacePrefix":"dkx","spaceId":12345,"spaceName":"测试空间","viewMode":"list","availableSpaces":[{"id":12345,"prefix":"dkx","name":"测试空间"}],"defaults":{"title":"修复登录验证码刷新问题","typeId":"5009","spaceId":12345}}
80
+ __interactive:icafe-cards {"cards":[{"sequence":"200","title":"修复登录问题","type":"Bug","status":"开发中"}],"spacePrefix":"dkx","spaceId":12345,"spaceName":"测试空间","viewMode":"list","availableSpaces":[{"id":12345,"prefix":"dkx","name":"测试空间","types":[{"id":"5009","name":"Bug"},{"id":"5007","name":"Story"}]}],"defaults":{"title":"修复登录验证码刷新问题","typeId":"5009","spaceId":12345}}
81
81
  ```
82
82
 
83
83
  ---
@@ -27,7 +27,8 @@ def run_git(args, cwd, timeout=10):
27
27
  try:
28
28
  result = subprocess.run(
29
29
  ["git"] + args,
30
- capture_output=True, text=True, timeout=timeout, cwd=cwd
30
+ capture_output=True, text=True, encoding="utf-8", errors="replace",
31
+ timeout=timeout, cwd=cwd
31
32
  )
32
33
  if result.returncode == 0:
33
34
  return result.stdout
@@ -86,22 +87,14 @@ def get_untracked_diff_and_stats(cwd, untracked_files):
86
87
  truncated = False
87
88
 
88
89
  for file_path in untracked_files:
90
+ full_path = os.path.join(cwd, file_path)
89
91
  try:
90
- result = subprocess.run(
91
- ["git", "diff", "--no-index", "/dev/null", file_path],
92
- capture_output=True, text=True, timeout=10, cwd=cwd
93
- )
94
- # git diff --no-index 在有差异时返回 1,不算失败
95
- file_diff = result.stdout
96
- except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
92
+ with open(full_path, "r", encoding="utf-8", errors="replace") as f:
93
+ lines = f.readlines()
94
+ except (OSError, IOError):
97
95
  continue
98
96
 
99
- # 统计 insertions:+ 开头但非 +++ 的行
100
- insertions = 0
101
- for line in file_diff.splitlines():
102
- if line.startswith("+") and not line.startswith("+++"):
103
- insertions += 1
104
-
97
+ insertions = len(lines)
105
98
  if insertions > 0:
106
99
  changed_files.append({
107
100
  "file": file_path,
@@ -109,17 +102,25 @@ def get_untracked_diff_and_stats(cwd, untracked_files):
109
102
  "deletions": 0,
110
103
  })
111
104
 
112
- # 拼接 diff 内容,检查总行数上限
113
- file_lines = file_diff.splitlines()
105
+ # 构造 unified diff 格式的输出
106
+ file_diff_lines = [
107
+ f"diff --git a/{file_path} b/{file_path}",
108
+ "new file mode 100644",
109
+ f"--- /dev/null",
110
+ f"+++ b/{file_path}",
111
+ f"@@ -0,0 +1,{insertions} @@",
112
+ ] + [f"+{line.rstrip()}" for line in lines]
113
+
114
+ # 检查总行数上限
114
115
  remaining = MAX_UNTRACKED_DIFF_LINES - total_lines
115
116
  if remaining <= 0:
116
117
  truncated = True
117
118
  break
118
- if len(file_lines) > remaining:
119
- file_lines = file_lines[:remaining]
119
+ if len(file_diff_lines) > remaining:
120
+ file_diff_lines = file_diff_lines[:remaining]
120
121
  truncated = True
121
- diff_lines.extend(file_lines)
122
- total_lines += len(file_lines)
122
+ diff_lines.extend(file_diff_lines)
123
+ total_lines += len(file_diff_lines)
123
124
 
124
125
  return changed_files, "\n".join(diff_lines), truncated
125
126
 
@@ -63,7 +63,8 @@ def get_current_user(username: Optional[str] = None) -> Optional[str]:
63
63
  try:
64
64
  result = subprocess.run(
65
65
  ["git", "config", "user.email"],
66
- capture_output=True, text=True, timeout=5
66
+ capture_output=True, text=True, encoding="utf-8", errors="replace",
67
+ timeout=5
67
68
  )
68
69
  if result.returncode == 0 and result.stdout.strip():
69
70
  username = result.stdout.strip().split("@")[0]
@@ -77,7 +78,8 @@ def get_current_user(username: Optional[str] = None) -> Optional[str]:
77
78
  try:
78
79
  result = subprocess.run(
79
80
  ["git", "config", "user.name"],
80
- capture_output=True, text=True, timeout=5
81
+ capture_output=True, text=True, encoding="utf-8", errors="replace",
82
+ timeout=5
81
83
  )
82
84
  if result.returncode == 0 and result.stdout.strip():
83
85
  username = result.stdout.strip()
@@ -112,7 +114,8 @@ def get_git_diff_summary(max_diff_lines: int = DEFAULT_MAX_DIFF_LINES) -> Option
112
114
  try:
113
115
  stat_result = subprocess.run(
114
116
  ["git", "diff", "HEAD", "--stat"],
115
- capture_output=True, text=True, timeout=10
117
+ capture_output=True, text=True, encoding="utf-8", errors="replace",
118
+ timeout=10
116
119
  )
117
120
  if stat_result.returncode != 0 or not stat_result.stdout.strip():
118
121
  logger.info("git diff 无变更")
@@ -136,7 +139,8 @@ def get_git_diff_summary(max_diff_lines: int = DEFAULT_MAX_DIFF_LINES) -> Option
136
139
  try:
137
140
  diff_result = subprocess.run(
138
141
  ["git", "diff", "HEAD"],
139
- capture_output=True, text=True, timeout=30
142
+ capture_output=True, text=True, encoding="utf-8", errors="replace",
143
+ timeout=30
140
144
  )
141
145
  diff_content = diff_result.stdout if diff_result.returncode == 0 else ""
142
146
  except (subprocess.TimeoutExpired, FileNotFoundError):
@@ -178,7 +182,8 @@ def extract_space_ids_from_git_log(
178
182
  try:
179
183
  result = subprocess.run(
180
184
  ["git", "log", "--oneline", "-n", str(max_commits)],
181
- capture_output=True, text=True, timeout=10
185
+ capture_output=True, text=True, encoding="utf-8", errors="replace",
186
+ timeout=10
182
187
  )
183
188
  if result.returncode != 0:
184
189
  return []
@@ -3,13 +3,15 @@
3
3
  包含常量、配置类和 API 客户端。
4
4
  """
5
5
 
6
+ import json as _json
6
7
  import os
8
+ import urllib.error
9
+ import urllib.parse
10
+ import urllib.request
7
11
  from dataclasses import dataclass
8
12
  from pathlib import Path
9
13
  from typing import Dict, List, Any, Optional
10
14
 
11
- import requests
12
-
13
15
  from logger import get_logger
14
16
 
15
17
  logger = get_logger()
@@ -126,34 +128,69 @@ class ICafeQueryClient:
126
128
 
127
129
  def __init__(self, config: Optional[ICafeQueryConfig] = None):
128
130
  self.config = config or ICafeQueryConfig()
129
- self.session = requests.Session()
130
-
131
- self.session.headers.update({
132
- "Content-Type": "application/json"
133
- })
131
+ self._default_headers: Dict[str, str] = {
132
+ "Content-Type": "application/json",
133
+ "User-Agent": "iAPI/1.0.0 (http://iapi.baidu-int.com)",
134
+ }
134
135
 
135
136
  auth_token = self.config._get_auth_token()
136
137
  if auth_token:
137
- self.session.headers["x-ac-Authorization"] = auth_token
138
+ self._default_headers["x-ac-Authorization"] = auth_token
138
139
 
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
- }
140
+ def _http_request(
141
+ self,
142
+ url: str,
143
+ params: Optional[dict] = None,
144
+ data: Optional[dict] = None,
145
+ headers: Optional[Dict[str, str]] = None,
146
+ timeout: Optional[int] = None,
147
+ ) -> Any:
148
+ """发送 HTTP 请求并返回解析后的 JSON
149
+
150
+ Args:
151
+ url: 请求 URL
152
+ params: GET 查询参数
153
+ data: POST 请求体(dict,会被序列化为 JSON)
154
+ headers: 额外 headers(会合并到默认 headers)
155
+ timeout: 超时时间(秒)
156
+
157
+ Returns:
158
+ 解析后的 JSON 对象(dict 或 list)
159
+
160
+ Raises:
161
+ urllib.error.HTTPError: HTTP 状态码非 2xx 时
162
+ urllib.error.URLError: 网络连接失败时
163
+ """
164
+ if timeout is None:
165
+ timeout = self.config.timeout
166
+
167
+ # 合并 headers
168
+ merged_headers = dict(self._default_headers)
169
+ if headers:
170
+ merged_headers.update(headers)
171
+
172
+ # 构建完整 URL
173
+ if params:
174
+ query_string = urllib.parse.urlencode(params, doseq=True)
175
+ url = f"{url}?{query_string}" if "?" not in url else f"{url}&{query_string}"
176
+
177
+ # 构建 Request
178
+ body = None
179
+ if data is not None:
180
+ body = _json.dumps(data).encode("utf-8")
181
+
182
+ req = urllib.request.Request(url, data=body, headers=merged_headers)
183
+
184
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
185
+ return _json.loads(resp.read().decode("utf-8"))
144
186
 
145
- def _get_with_retry(self, url: str, params: dict, max_retries: int = 3) -> requests.Response:
146
- """带重试的 GET 请求"""
187
+ def _get_with_retry(self, url: str, params: dict, max_retries: int = 3) -> Any:
188
+ """带重试的 GET 请求,返回解析后的 JSON"""
147
189
  import time
148
190
  last_exc = None
149
191
  for attempt in range(max_retries + 1):
150
192
  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
193
+ return self._http_request(url, params=params)
157
194
  except Exception as e:
158
195
  last_exc = e
159
196
  if attempt < max_retries:
@@ -195,7 +232,7 @@ class ICafeQueryClient:
195
232
 
196
233
  Raises:
197
234
  ValueError: 当 space_id 或 iql 为空时
198
- requests.RequestException: 请求失败时
235
+ urllib.error.URLError: 请求失败时
199
236
 
200
237
  IQL 表达式示例:
201
238
  - "类型 = Bug"
@@ -234,8 +271,7 @@ class ICafeQueryClient:
234
271
 
235
272
  url = f"{self.config.base_url}/api/spaces/{space_id}/cards"
236
273
  logger.info("查询卡片: space=%s, iql=%s", space_id, iql)
237
- response = self._get_with_retry(url, params)
238
- result = response.json()
274
+ result = self._get_with_retry(url, params)
239
275
  cards_count = len(result.get("cards", []))
240
276
  logger.info("查询卡片返回: %d 张", cards_count)
241
277
  return result
@@ -263,7 +299,7 @@ class ICafeQueryClient:
263
299
 
264
300
  Raises:
265
301
  ValueError: 当 space_id 或 card_id 为空时
266
- requests.RequestException: 请求失败时
302
+ urllib.error.URLError: 请求失败时
267
303
  """
268
304
  if not space_id or not card_id:
269
305
  raise ValueError("space_id 和 card_id 不能为空")
@@ -279,8 +315,7 @@ class ICafeQueryClient:
279
315
  params['showAccumulate'] = ''
280
316
 
281
317
  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()
318
+ return self._get_with_retry(url, params)
284
319
 
285
320
  # ========================================
286
321
  # 3. 查询最近访问的空间列表
@@ -297,7 +332,7 @@ class ICafeQueryClient:
297
332
 
298
333
  Raises:
299
334
  ValueError: 当 current_user 为空时
300
- requests.RequestException: 请求失败时
335
+ urllib.error.URLError: 请求失败时
301
336
  """
302
337
  if not current_user:
303
338
  raise ValueError("current_user 不能为空")
@@ -305,8 +340,7 @@ class ICafeQueryClient:
305
340
  logger.info("查询最近访问空间: user=%s, limit=%d", current_user, limit)
306
341
  url = f"{self.config.base_url}/api/v2/space/latest"
307
342
  params = {'currentUser': current_user}
308
- response = self._get_with_retry(url, params)
309
- data = response.json()
343
+ data = self._get_with_retry(url, params)
310
344
 
311
345
  # 对返回的空间列表截取前 limit 个
312
346
  if isinstance(data, dict) and 'result' in data:
@@ -330,14 +364,13 @@ class ICafeQueryClient:
330
364
 
331
365
  Raises:
332
366
  ValueError: 当 space_id 为空时
333
- requests.RequestException: 请求失败时
367
+ urllib.error.URLError: 请求失败时
334
368
  """
335
369
  if not space_id:
336
370
  raise ValueError("space_id 不能为空")
337
371
 
338
372
  url = f"{self.config.base_url}/api/v2/space/{space_id}/plans"
339
- response = self._get_with_retry(url, {})
340
- return response.json()
373
+ return self._get_with_retry(url, {})
341
374
 
342
375
  # ========================================
343
376
  # 5. 获取空间卡片类型列表
@@ -353,14 +386,13 @@ class ICafeQueryClient:
353
386
 
354
387
  Raises:
355
388
  ValueError: 当 space_prefix 为空时
356
- requests.RequestException: 请求失败时
389
+ urllib.error.URLError: 请求失败时
357
390
  """
358
391
  if not space_prefix:
359
392
  raise ValueError("space_prefix 不能为空")
360
393
 
361
394
  url = f"{self.config.base_url}/api/v2/space/{space_prefix}/issueTypes"
362
- response = self._get_with_retry(url, {})
363
- return response.json()
395
+ return self._get_with_retry(url, {})
364
396
 
365
397
  # ========================================
366
398
  # 6. 创建卡片
@@ -421,10 +453,7 @@ class ICafeQueryClient:
421
453
  'Content-Type': 'application/json'
422
454
  }
423
455
 
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()
456
+ result = self._http_request(url, data=data, headers=headers)
428
457
 
429
458
  # 检查返回的状态
430
459
  if isinstance(result, dict):
@@ -1,6 +1,8 @@
1
1
  """Farseer API 客户端"""
2
2
 
3
- import requests
3
+ import json
4
+ import urllib.parse
5
+ import urllib.request
4
6
  from typing import Dict, List, Any, Optional
5
7
 
6
8
  from .client import FARSEER_BASE_URL, DEFAULT_TIMEOUT, ISSUE_TYPE_MAP
@@ -25,14 +27,11 @@ def get_binding_card_types(space_id: int, timeout: int = DEFAULT_TIMEOUT) -> Lis
25
27
  """
26
28
  logger.info("查询 farseer 可绑定类型: space_id=%s", space_id)
27
29
  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()
30
+ query_string = urllib.parse.urlencode({'icafeSpaceId': space_id})
31
+ url = f"{FARSEER_BASE_URL}/storyRule/getRule?{query_string}"
32
+ req = urllib.request.Request(url)
33
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
34
+ data = json.loads(resp.read().decode("utf-8"))
36
35
 
37
36
  rule_data = data.get('data', {}).get('data', {})
38
37
  binding_types = rule_data.get('engineeringBindingTypes', [])
@@ -622,7 +622,10 @@ def find_matching_card(
622
622
  "space_name": "空间名称",
623
623
  "final_iql": "...",
624
624
  "available_types": [{"id": "5009", "name": "Bug"}, ...],
625
- "available_spaces": [{"id": 12345, "prefix": "dkx", "name": "空间名称"}, ...],
625
+ "available_spaces": [
626
+ {"id": 12345, "prefix": "dkx", "name": "空间名称",
627
+ "types": [{"id": "5009", "name": "Bug"}]}, ...
628
+ ],
626
629
  "defaults": {
627
630
  "title": "", # 由 Agent 根据 diff 生成
628
631
  "type_id": "5009", # 默认卡片类型 ID
@@ -0,0 +1,37 @@
1
+ """命令行工具:查询 iCafe 匹配卡片
2
+
3
+ 用法:
4
+ python3 match_card_cli.py --username "dongkexin01"
5
+
6
+ 输出:
7
+ JSON 格式的匹配结果,包含 cards、space_prefix、available_types 等字段。
8
+ 失败时输出 {"error": "错误信息"} 或 {"disabled": true, "message": "..."}。
9
+ """
10
+
11
+ import argparse
12
+ import json
13
+ import os
14
+ import sys
15
+
16
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
17
+
18
+ from icafe.matching import find_matching_card
19
+
20
+
21
+ def main():
22
+ """解析命令行参数并查询匹配卡片。"""
23
+ parser = argparse.ArgumentParser(description="查询 iCafe 匹配卡片")
24
+ parser.add_argument("--username", default=None, help="用户名,如 dongkexin01")
25
+ args = parser.parse_args()
26
+
27
+ try:
28
+ username = args.username or os.environ.get("COMATE_USERNAME")
29
+ result = find_matching_card(current_user=username)
30
+ print(json.dumps(result, ensure_ascii=False))
31
+ except Exception as e:
32
+ print(json.dumps({"error": f"查询匹配卡片异常: {type(e).__name__}: {e}"}))
33
+ sys.exit(1)
34
+
35
+
36
+ if __name__ == "__main__":
37
+ main()
@@ -273,10 +273,10 @@ When creating a new skill from scratch, always run the `init_skill.py` script. T
273
273
 
274
274
  Storage:
275
275
 
276
- | Type | Path | Scope |
277
- |------|------|-------|
278
- | Personal | ~/.comate/skills/skill-name/ | Available across all your projects |
279
- | Project | .comate/skills/skill-name/ | Shared with anyone using the repository |
276
+ | Type | Path | Scope |
277
+ | -------- | ---------------------------- | --------------------------------------- |
278
+ | Personal | ~/.comate/skills/skill-name/ | Available across all your projects |
279
+ | Project | .comate/skills/skill-name/ | Shared with anyone using the repository |
280
280
 
281
281
  **IMPORTANT**: Never create or update skills in `~/.comate/skills/skill-name-comate/`. This directory is reserved for Comate's internal built-in skills and is managed automatically by the system.
282
282
 
@@ -346,7 +346,6 @@ scripts/quick_validate.py <path/to/skill-folder>
346
346
 
347
347
  The validation script checks YAML frontmatter format, required fields, and naming rules. If validation fails, fix the reported issues and run the command again.
348
348
 
349
-
350
349
  ### Step 6: Iterate
351
350
 
352
351
  After testing the skill, users may request improvements. Often this happens right after using the skill, with fresh context of how the skill performed.