@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.
- package/comate-engine/assets/skills/auto-commit/SKILL.md +74 -24
- package/comate-engine/assets/skills/auto-commit/references/new_version_instruction.md +1 -1
- package/comate-engine/assets/skills/auto-commit/scripts/git_diff_cli.py +21 -20
- package/comate-engine/assets/skills/auto-commit/scripts/git_utils.py +10 -5
- package/comate-engine/assets/skills/auto-commit/scripts/icafe/client.py +69 -40
- package/comate-engine/assets/skills/auto-commit/scripts/icafe/farseer.py +8 -9
- package/comate-engine/assets/skills/auto-commit/scripts/icafe/matching.py +4 -1
- package/comate-engine/assets/skills/auto-commit/scripts/match_card_cli.py +37 -0
- package/comate-engine/assets/skills/create-skill-comate/SKILL.md +4 -5
- package/comate-engine/node_modules/@comate/plugin-engine/dist/index.js +1 -1
- package/comate-engine/node_modules/@comate/plugin-host/dist/index-QEN4ay0E.js +1 -0
- 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-host/dist/user-DAIE9qbz.js +44 -0
- package/comate-engine/node_modules/@comate/plugin-shared-internals/dist/index.js +2 -2
- package/comate-engine/server.js +76 -136
- package/dist/bundle/index.js +3 -3
- package/package.json +1 -1
|
@@ -30,23 +30,9 @@ metadata:
|
|
|
30
30
|
- "当前版本不支持..."
|
|
31
31
|
- 任何提及"版本"、"降级"、"兼容"、"开关"等字眼
|
|
32
32
|
|
|
33
|
-
### 0a.
|
|
33
|
+
### 0a. 版本兼容性检查(纯文本判断,无需命令)
|
|
34
34
|
|
|
35
|
-
|
|
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: ]
|
|
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,
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
113
|
-
|
|
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(
|
|
119
|
-
|
|
119
|
+
if len(file_diff_lines) > remaining:
|
|
120
|
+
file_diff_lines = file_diff_lines[:remaining]
|
|
120
121
|
truncated = True
|
|
121
|
-
diff_lines.extend(
|
|
122
|
-
total_lines += len(
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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.
|
|
138
|
+
self._default_headers["x-ac-Authorization"] = auth_token
|
|
138
139
|
|
|
139
|
-
def
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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) ->
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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": [
|
|
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
|
|
277
|
-
|
|
278
|
-
| Personal | ~/.comate/skills/skill-name/ | Available across all your projects
|
|
279
|
-
| Project
|
|
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.
|