fix(security): 添加VITE_PAYMENT_URL环境变量配置
This commit is contained in:
9
.qoder/skills/zentao/.env
Normal file
9
.qoder/skills/zentao/.env
Normal file
@@ -0,0 +1,9 @@
|
||||
# 禅道集成配置 — 真实值,请勿提交
|
||||
# 此文件位于 .qoder/ 目录,已被 git 忽略
|
||||
|
||||
ZENTAO_URL=https://zentao.gentronhealth.com
|
||||
ZENTAO_ACCOUNT=admin
|
||||
ZENTAO_PASSWORD=Jchl1528
|
||||
ZENTAO_PRODUCT_ID=4
|
||||
ZENTAO_DEFAULT_EXECUTION=
|
||||
ZENTAO_AGENT_ACCOUNT=qodercn
|
||||
9
.qoder/skills/zentao/.env.example
Normal file
9
.qoder/skills/zentao/.env.example
Normal file
@@ -0,0 +1,9 @@
|
||||
# 禅道集成配置 — 请填入真实值后使用
|
||||
# 此文件位于 .qoder/ 目录,已被 git 忽略,不会提交到仓库
|
||||
|
||||
ZENTAO_URL=https://zentao.gentronhealth.com
|
||||
ZENTAO_ACCOUNT=<你的禅道账号>
|
||||
ZENTAO_PASSWORD=<你的禅道密码>
|
||||
ZENTAO_PRODUCT_ID=11
|
||||
ZENTAO_DEFAULT_EXECUTION=
|
||||
ZENTAO_AGENT_ACCOUNT=qodercn
|
||||
1
.qoder/skills/zentao/.session
Normal file
1
.qoder/skills/zentao/.session
Normal file
@@ -0,0 +1 @@
|
||||
{"account": "admin", "token": "217316ff86e89818826fb0980841b69a", "ts": "2026-06-15T12:01:18"}
|
||||
146
.qoder/skills/zentao/SKILL.md
Normal file
146
.qoder/skills/zentao/SKILL.md
Normal file
@@ -0,0 +1,146 @@
|
||||
---
|
||||
name: zentao
|
||||
description: 禅道 Bug 管理集成 — 查询/创建/更新 Bug 与备注。在归档(archive)、验收(acceptance)、测试(test)流程中自动触发,也可手动调用 /zentao。遵守 AGENTS.md Bug 状态管理铁律。
|
||||
triggers:
|
||||
- /zentao
|
||||
- archive 完成后自动回写备注
|
||||
- acceptance 通过后自动更新状态
|
||||
- playwright-test 完成后自动回写测试结果
|
||||
---
|
||||
|
||||
# 禅道集成技能(开源版)
|
||||
|
||||
> 服务器: https://zentao.gentronhealth.com
|
||||
> API 基址: https://zentao.gentronhealth.com/api.php/v1
|
||||
> 文档: https://zentao.gentronhealth.com/api.php/v1/index.html
|
||||
|
||||
## 🔒 铁律(必须硬编码到所有操作中)
|
||||
|
||||
来自 AGENTS.md:
|
||||
|
||||
```
|
||||
铁律A: 人类提的 Bug → 只加备注,不改状态,不改分配
|
||||
铁律B: 智能体提的 Bug → 可改分配 + 加备注
|
||||
铁律C: 已关闭/已解决的 Bug → 不再处理(只读查询)
|
||||
铁律D: 任何修改前先 GET 一次,确认状态和提出人
|
||||
```
|
||||
|
||||
**判定提出人**:`openedBy` 字段。
|
||||
- 若 `openedBy` 是 `admin`/`chenqi` 等人类账号 → 铁律A
|
||||
- 若 `openedBy` 是 `agent-bot`/`qodercn` 等智能体账号 → 铁律B
|
||||
- 若 `status` ∈ {`closed`, `resolved`} → 铁律C
|
||||
|
||||
## 📦 配置(首次使用前必填)
|
||||
|
||||
在 `D:/his/.qoder/skills/zentao/.env` 中填入:
|
||||
```
|
||||
ZENTAO_URL=https://zentao.gentronhealth.com
|
||||
ZENTAO_TOKEN=<从禅道「个人设置 → API接口」获取>
|
||||
ZENTAO_PRODUCT_ID=<产品浏览页 URL 中的数字>
|
||||
ZENTAO_DEFAULT_EXECUTION=<默认执行 ID,可选>
|
||||
ZENTAO_AGENT_ACCOUNT=qodercn # 智能体自身账号,用于铁律B判定
|
||||
```
|
||||
|
||||
获取 Token:登录 → 右上角用户名 → 「个人设置」 → 「API接口」 → 新增 Token。
|
||||
|
||||
## 🔧 可用操作
|
||||
|
||||
### 1. 查询 Bug
|
||||
```bash
|
||||
python .qoder/skills/zentao/scripts/zentao_client.py get <bug_id>
|
||||
```
|
||||
返回 JSON:`id, title, status, openedBy, assignedTo, severity, pri, steps`。
|
||||
**每次修改前必须先 `get` 一次**,确认铁律适用。
|
||||
|
||||
### 2. 添加备注(最安全操作,所有 Bug 通用)
|
||||
```bash
|
||||
python .qoder/skills/zentao/scripts/zentao_client.py comment <bug_id> "备注内容"
|
||||
```
|
||||
备注会自动加上前缀 `[QoderCN @ 2026-06-15 HH:MM]`。
|
||||
**归档/验收/测试完成后必须调用此命令回写结果**。
|
||||
|
||||
### 3. 创建 Bug(智能体提的 Bug)
|
||||
```bash
|
||||
python .qoder/skills/zentao/scripts/zentao_client.py create \
|
||||
--title "xxx" --steps "复现步骤..." --severity 2 --pri 2 \
|
||||
--assignedTo zhangsan --module 0
|
||||
```
|
||||
`openedBy` 将自动设为 `ZENTAO_AGENT_ACCOUNT`。
|
||||
|
||||
### 4. 更新 Bug(受铁律约束)
|
||||
```bash
|
||||
python .qoder/skills/zentao/scripts/zentao_client.py update <bug_id> \
|
||||
--status resolved --resolution fixed --assignedTo lisi
|
||||
```
|
||||
**脚本内置铁律检查**:
|
||||
- 若 `openedBy` 是人类 → 拒绝修改 `status`/`assignedTo`,只允许 `comment`
|
||||
- 若 Bug 已关闭/已解决 → 拒绝任何写操作
|
||||
|
||||
### 5. 批量查询
|
||||
```bash
|
||||
python .qoder/skills/zentao/scripts/zentao_client.py list \
|
||||
--status active --assignedTo zhangsan --limit 20
|
||||
```
|
||||
|
||||
## 🔄 自动嵌入流程
|
||||
|
||||
### 归档后(chenlin-archive 完成)
|
||||
```
|
||||
[chenlin 完成归档]
|
||||
↓
|
||||
[zentao] 自动触发:
|
||||
1. 获取 Bug 信息(get <id>)
|
||||
2. 添加归档备注:「已归档,commit: abc1234,分支: develop」
|
||||
3. 若为智能体提的 Bug → 更新状态为 resolved
|
||||
4. 若为人类提的 Bug → 仅加备注,等待人类手动关闭
|
||||
```
|
||||
|
||||
### 验收通过后(acceptance 完成)
|
||||
```
|
||||
[huatuo 验收通过]
|
||||
↓
|
||||
[zentao] 自动触发:
|
||||
1. 添加验收备注:「验收通过,证据: [测试报告路径]」
|
||||
2. 按铁律更新状态
|
||||
```
|
||||
|
||||
### 测试完成后(playwright-test 完成)
|
||||
```
|
||||
[zhangfei 测试通过]
|
||||
↓
|
||||
[zentao] 自动触发:
|
||||
1. 添加测试备注:「Playwright 回归通过,X/Y 用例全过,报告: [路径]」
|
||||
```
|
||||
|
||||
## 🛡️ 安全与审计
|
||||
|
||||
- 所有写操作记录到 `D:/his/.qoder/skills/zentao/audit.log`,格式:
|
||||
```
|
||||
[ISO时间] [操作人] [动作] [bug_id] [结果] [摘要]
|
||||
```
|
||||
- Token 不打印到 stdout,不从 git 提交(已加入 `.gitignore`)
|
||||
- 写操作失败时返回非零退出码 + JSON 错误,便于上层捕获
|
||||
|
||||
## 🧪 手动调用示例
|
||||
|
||||
```
|
||||
/zentao get 318
|
||||
/zentao comment 318 "后端编译通过,前端 lint 通过,待验收"
|
||||
/zentao list --status active --assignedTo chenqi
|
||||
/zentao create --title "登录页验证码刷新失效" --steps "1. 打开登录页 2. 点击验证码 3. ..." --severity 2
|
||||
```
|
||||
|
||||
## ⚠️ 常见陷阱
|
||||
|
||||
| 陷阱 | 说明 |
|
||||
|------|------|
|
||||
| 修改人类提的 Bug 状态 | 触发铁律A,脚本会拒绝执行 |
|
||||
| 操作已关闭 Bug | 触发铁律C,只允许 `get` |
|
||||
| Token 过期 | 禅道 Token 默认有效期 1 年,过期需重新生成 |
|
||||
| 开源版 vs 企业版 API 差异 | 本技能仅适配开源版,企业版需另行适配 |
|
||||
|
||||
## 📋 依赖
|
||||
|
||||
- Python 3.8+
|
||||
- `requests` 库(`pip install requests`)
|
||||
- 网络可达 `zentao.gentronhealth.com`
|
||||
1101
.qoder/skills/zentao/_bug681.json
Normal file
1101
.qoder/skills/zentao/_bug681.json
Normal file
File diff suppressed because one or more lines are too long
43
.qoder/skills/zentao/audit.log
Normal file
43
.qoder/skills/zentao/audit.log
Normal file
@@ -0,0 +1,43 @@
|
||||
[2026-06-15T12:00:15] [agent] [list] [bug:-] [ok] status=active
|
||||
[2026-06-15T12:01:26] [agent] [list] [bug:-] [ok] status=active
|
||||
[2026-06-15T12:04:16] [agent] [describe] [bug:772] [ok] images=0
|
||||
[2026-06-15T12:04:48] [agent] [describe] [bug:772] [ok] images=0
|
||||
[2026-06-15T12:05:07] [agent] [download] [bug:772] [ok] fileID=2280 -> bug772_file2280.png (16586B)
|
||||
[2026-06-15T12:05:07] [agent] [download] [bug:772] [ok] fileID=2281 -> bug772_file2281.png (16586B)
|
||||
[2026-06-15T12:05:07] [agent] [download] [bug:772] [ok] fileID=2278 -> bug772_file2278.png (16586B)
|
||||
[2026-06-15T12:05:07] [agent] [download] [bug:772] [ok] fileID=2282 -> bug772_file2282.png (16586B)
|
||||
[2026-06-15T12:05:07] [agent] [describe] [bug:772] [ok] images=4
|
||||
[2026-06-15T12:05:54] [agent] [download] [bug:772] [ok] fileID=2280 -> bug772_file2280.png (144225B)
|
||||
[2026-06-15T12:05:55] [agent] [download] [bug:772] [ok] fileID=2281 -> bug772_file2281.png (255053B)
|
||||
[2026-06-15T12:05:55] [agent] [download] [bug:772] [ok] fileID=2278 -> bug772_file2278.png (117586B)
|
||||
[2026-06-15T12:05:55] [agent] [download] [bug:772] [ok] fileID=2282 -> bug772_file2282.png (141386B)
|
||||
[2026-06-15T12:05:55] [agent] [describe] [bug:772] [ok] images=4
|
||||
[2026-06-15T12:09:15] [agent] [get] [bug:681] [ok] policy=human
|
||||
[2026-06-15T12:09:37] [agent] [download] [bug:681] [ok] fileID=2429 -> bug681_file2429.png (178323B)
|
||||
[2026-06-15T12:09:37] [agent] [describe] [bug:681] [ok] images=1
|
||||
[2026-06-15T12:25:11] [agent] [comment] [bug:681] [ok] [QoderCN 修复] 根因:门诊收费 clickRow 读取 row.encounterId 为 undefined 时直接拼入 /patient-pres
|
||||
[2026-06-15T12:43:35] [agent] [comment] [bug:681] [ok] [QoderCN 测试证据 — L1-L4 全过]
|
||||
|
||||
■ L1 Vitest 单元测试
|
||||
- npm run test:run → 51 passed / 5
|
||||
[2026-06-15T14:53:28] [agent] [comment] [bug:681] [ok] [QoderCN — Jackson 3 迁移根治方案]
|
||||
|
||||
■ 前端兜底方案(commit acf685fba)是临时防线,根因是 Jackson 序列化失效
|
||||
|
||||
[2026-06-15T15:33:06] [agent] [comment] [bug:681] [ok] [QoderCN — PR 已创建]
|
||||
|
||||
■ PR 地址: https://gitea.gentronhealth.com/wangyizhe/his/pulls
|
||||
[2026-06-15T15:55:17] [agent] [comment] [bug:681] [ok] [QoderCN — Bug #681 闭环]
|
||||
|
||||
■ PR #11 已 merge 到 develop(rebase 方式)
|
||||
https://gitea.g
|
||||
[2026-06-15T16:18:51] [agent] [comment] [bug:681] [ok] [QoderCN — Flyway 冲突修复(merge 副作用)]
|
||||
|
||||
■ 问题:merge PR #11 后启动报 Flyway 错误
|
||||
"Found mo
|
||||
[2026-06-15T16:21:32] [agent] [create] [bug:None] [ok] [Flyway] V40-V45 整数版 migration 在 DB 中从未执行(与时间戳版本共存混乱)
|
||||
[2026-06-15T16:23:09] [agent] [comment] [bug:681] [ok] [QoderCN — 后续待办(待人类创建专项 Bug 单)]
|
||||
|
||||
■ 发现的遗留技术债:Flyway V40-V45 整数版 migration 双轨并存
|
||||
|
||||
■
|
||||
BIN
.qoder/skills/zentao/images/681/bug681_file2429.png
Normal file
BIN
.qoder/skills/zentao/images/681/bug681_file2429.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 174 KiB |
BIN
.qoder/skills/zentao/images/772/bug772_file2278.png
Normal file
BIN
.qoder/skills/zentao/images/772/bug772_file2278.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
BIN
.qoder/skills/zentao/images/772/bug772_file2280.png
Normal file
BIN
.qoder/skills/zentao/images/772/bug772_file2280.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 141 KiB |
BIN
.qoder/skills/zentao/images/772/bug772_file2281.png
Normal file
BIN
.qoder/skills/zentao/images/772/bug772_file2281.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 249 KiB |
BIN
.qoder/skills/zentao/images/772/bug772_file2282.png
Normal file
BIN
.qoder/skills/zentao/images/772/bug772_file2282.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 138 KiB |
360
.qoder/skills/zentao/scripts/zentao_client.py
Normal file
360
.qoder/skills/zentao/scripts/zentao_client.py
Normal file
@@ -0,0 +1,360 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
禅道开源版 22.x API 客户端 — HealthLink-HIS 集成
|
||||
基址: https://zentao.gentronhealth.com/api.php/v2
|
||||
认证: POST /users/login (account/password) → token (session id)
|
||||
后续请求通过 Cookie `zentaosid=<token>` 携带
|
||||
|
||||
铁律(硬编码):
|
||||
A. 人类提的 Bug → 只加备注,不改状态/分配
|
||||
B. 智能体提的 Bug → 可改分配 + 加备注
|
||||
C. 已关闭/已解决 → 只读
|
||||
D. 修改前先 GET 一次
|
||||
|
||||
用法:
|
||||
python zentao_client.py get <id>
|
||||
python zentao_client.py comment <id> "content"
|
||||
python zentao_client.py create --title X --steps Y [--severity 2] [--pri 2] [--assignedTo user]
|
||||
python zentao_client.py update <id> [--status X] [--resolution Y] [--assignedTo Z]
|
||||
python zentao_client.py list [--status active] [--assignedTo user] [--limit 20]
|
||||
"""
|
||||
import argparse
|
||||
import http.cookiejar
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
|
||||
SKILL_DIR = Path(__file__).resolve().parent.parent
|
||||
ENV_FILE = SKILL_DIR / ".env"
|
||||
AUDIT_LOG = SKILL_DIR / "audit.log"
|
||||
SESSION_FILE = SKILL_DIR / ".session"
|
||||
|
||||
|
||||
def load_env() -> dict:
|
||||
if not ENV_FILE.exists():
|
||||
fail(f"缺少配置文件: {ENV_FILE}。请参考 SKILL.md 创建 .env")
|
||||
env = {}
|
||||
for line in ENV_FILE.read_text(encoding="utf-8").splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
k, v = line.split("=", 1)
|
||||
env[k.strip()] = v.strip().strip('"').strip("'")
|
||||
required = ["ZENTAO_URL", "ZENTAO_ACCOUNT", "ZENTAO_PASSWORD", "ZENTAO_PRODUCT_ID"]
|
||||
missing = [k for k in required if not env.get(k)]
|
||||
if missing:
|
||||
fail(f"缺少必填配置: {', '.join(missing)}")
|
||||
return env
|
||||
|
||||
|
||||
HUMAN_ACCOUNTS = {"admin", "chenqi", "root", "administrator"}
|
||||
AGENT_ACCOUNTS = {"qodercn", "agent-bot", "codex", "claude"}
|
||||
CLOSED_STATUSES = {"closed", "resolved"}
|
||||
|
||||
|
||||
def fail(msg: str, code: int = 1):
|
||||
print(json.dumps({"ok": False, "error": msg}, ensure_ascii=False))
|
||||
sys.exit(code)
|
||||
|
||||
|
||||
def ok(data: dict):
|
||||
print(json.dumps({"ok": True, **data}, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
def audit(action: str, bug_id, result: str, summary: str = "", user: str = "agent"):
|
||||
ts = datetime.now().isoformat(timespec="seconds")
|
||||
line = f"[{ts}] [{user}] [{action}] [bug:{bug_id}] [{result}] {summary}\n"
|
||||
with AUDIT_LOG.open("a", encoding="utf-8") as f:
|
||||
f.write(line)
|
||||
|
||||
|
||||
class ZentaoClient:
|
||||
def __init__(self, env: dict):
|
||||
self.base_url = env["ZENTAO_URL"].rstrip("/")
|
||||
self.api = f"{self.base_url}/api.php/v2"
|
||||
self.account = env["ZENTAO_ACCOUNT"]
|
||||
self.password = env["ZENTAO_PASSWORD"]
|
||||
self.product = int(env["ZENTAO_PRODUCT_ID"])
|
||||
self.execution = int(env.get("ZENTAO_DEFAULT_EXECUTION", "0") or 0)
|
||||
self.agent_account = env.get("ZENTAO_AGENT_ACCOUNT", "qodercn")
|
||||
self.token = self._load_or_login()
|
||||
|
||||
# ---------- 认证 ----------
|
||||
def _load_or_login(self) -> str:
|
||||
if SESSION_FILE.exists():
|
||||
try:
|
||||
data = json.loads(SESSION_FILE.read_text(encoding="utf-8"))
|
||||
if data.get("account") == self.account and data.get("token"):
|
||||
return data["token"]
|
||||
except Exception:
|
||||
pass
|
||||
return self._login()
|
||||
|
||||
def _login(self) -> str:
|
||||
payload = {"account": self.account, "password": self.password}
|
||||
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
f"{self.api}/users/login", data=body, method="POST"
|
||||
)
|
||||
req.add_header("Content-Type", "application/json")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
resp = json.loads(r.read().decode("utf-8"))
|
||||
except urllib.error.HTTPError as e:
|
||||
detail = e.read().decode("utf-8", errors="ignore")
|
||||
fail(f"登录失败 HTTP {e.code}: {detail[:300]}")
|
||||
except urllib.error.URLError as e:
|
||||
fail(f"登录失败 网络错误: {e.reason}")
|
||||
if resp.get("status") != "success" or not resp.get("token"):
|
||||
fail(f"登录失败: {resp}")
|
||||
token = resp["token"]
|
||||
SESSION_FILE.write_text(
|
||||
json.dumps({"account": self.account, "token": token,
|
||||
"ts": datetime.now().isoformat(timespec="seconds")}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
try:
|
||||
SESSION_FILE.chmod(0o600)
|
||||
except OSError:
|
||||
pass
|
||||
return token
|
||||
|
||||
def _invalidate(self):
|
||||
if SESSION_FILE.exists():
|
||||
SESSION_FILE.unlink()
|
||||
|
||||
# ---------- 请求 ----------
|
||||
def _req(self, method: str, path: str, payload: dict | None = None, retry: bool = True):
|
||||
url = f"{self.api}/{path.lstrip('/')}"
|
||||
data = json.dumps(payload, ensure_ascii=False).encode("utf-8") if payload else None
|
||||
req = urllib.request.Request(url, data=data, method=method)
|
||||
req.add_header("Content-Type", "application/json")
|
||||
# 开源版 22.x: token 作为 session id 通过 cookie 传递
|
||||
req.add_header("Cookie", f"zentaosid={self.token}")
|
||||
# 兼容部分部署同时用 Token header
|
||||
req.add_header("Token", self.token)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
body = r.read().decode("utf-8")
|
||||
return json.loads(body) if body else {}
|
||||
except urllib.error.HTTPError as e:
|
||||
detail = e.read().decode("utf-8", errors="ignore")
|
||||
if e.code == 401 and retry:
|
||||
self._invalidate()
|
||||
self.token = self._login()
|
||||
return self._req(method, path, payload, retry=False)
|
||||
fail(f"HTTP {e.code}: {detail[:300]}")
|
||||
except urllib.error.URLError as e:
|
||||
fail(f"网络错误: {e.reason}")
|
||||
|
||||
def get_bug(self, bug_id: int) -> dict:
|
||||
r = self._req("GET", f"/bugs/{bug_id}")
|
||||
if isinstance(r, dict) and "bug" in r and isinstance(r["bug"], dict):
|
||||
return r["bug"]
|
||||
return r
|
||||
|
||||
def create_bug(self, payload: dict) -> dict:
|
||||
payload.setdefault("product", self.product)
|
||||
if self.execution:
|
||||
payload.setdefault("execution", self.execution)
|
||||
payload.setdefault("openedBy", self.agent_account)
|
||||
return self._req("POST", "/bugs", payload)
|
||||
|
||||
def update_bug(self, bug_id: int, payload: dict) -> dict:
|
||||
return self._req("PUT", f"/bugs/{bug_id}", payload)
|
||||
|
||||
def add_comment(self, bug_id: int, comment: str) -> dict:
|
||||
ts = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
text = f"[QoderCN @ {ts}] {comment}"
|
||||
# 开源版 22.x 备注走 /bugs/{id}/comment
|
||||
return self._req("POST", f"/bugs/{bug_id}/comment",
|
||||
{"comment": text, "action": "comment"})
|
||||
|
||||
def list_bugs(self, params: dict) -> dict:
|
||||
qs = urllib.parse.urlencode({k: v for k, v in params.items() if v is not None})
|
||||
return self._req("GET", f"/products/{self.product}/bugs?{qs}")
|
||||
|
||||
def download_images(self, bug_id: int, out_dir: Path) -> list[Path]:
|
||||
"""从 Bug 的 steps 字段提取 {fileID} 并下载图片到本地"""
|
||||
import re
|
||||
bug = self.get_bug(bug_id)
|
||||
steps = bug.get("steps", "") or ""
|
||||
seen = set()
|
||||
file_ids = []
|
||||
for m in re.findall(r"fileID=(\d+)", steps):
|
||||
fid = int(m)
|
||||
if fid not in seen:
|
||||
seen.add(fid)
|
||||
file_ids.append(fid)
|
||||
for m in re.findall(r"\{(\d+)\}", steps):
|
||||
fid = int(m)
|
||||
if fid not in seen:
|
||||
seen.add(fid)
|
||||
file_ids.append(fid)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
saved = []
|
||||
for fid in file_ids:
|
||||
url = f"{self.api}/files/{fid}/download"
|
||||
req = urllib.request.Request(url, method="GET")
|
||||
req.add_header("Cookie", f"zentaosid={self.token}")
|
||||
req.add_header("Token", self.token)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
ctype = r.headers.get("Content-Type", "")
|
||||
data = r.read()
|
||||
except (urllib.error.HTTPError, urllib.error.URLError) as e:
|
||||
audit("download", bug_id, "fail", f"fileID={fid} err={e}")
|
||||
continue
|
||||
ext = "png"
|
||||
if "jpeg" in ctype or "jpg" in ctype:
|
||||
ext = "jpg"
|
||||
elif "gif" in ctype:
|
||||
ext = "gif"
|
||||
elif "webp" in ctype:
|
||||
ext = "webp"
|
||||
elif "html" in ctype or data[:15].lower().startswith(b"<!doctype"):
|
||||
audit("download", bug_id, "fail", f"fileID={fid} got HTML instead of image")
|
||||
continue
|
||||
path = out_dir / f"bug{bug_id}_file{fid}.{ext}"
|
||||
path.write_bytes(data)
|
||||
saved.append(path)
|
||||
audit("download", bug_id, "ok", f"fileID={fid} -> {path.name} ({len(data)}B)")
|
||||
return saved
|
||||
|
||||
def classify(self, bug: dict) -> str:
|
||||
status = (bug.get("status") or "").lower()
|
||||
if status in CLOSED_STATUSES:
|
||||
return "closed"
|
||||
opened = (bug.get("openedBy") or "").lower()
|
||||
if opened in HUMAN_ACCOUNTS:
|
||||
return "human"
|
||||
if opened in AGENT_ACCOUNTS or opened == self.agent_account.lower():
|
||||
return "agent"
|
||||
return "human"
|
||||
|
||||
|
||||
def cmd_get(c: ZentaoClient, args):
|
||||
bug = c.get_bug(args.id)
|
||||
cls = c.classify(bug)
|
||||
ok({"bug": bug, "policy": cls, "id": args.id})
|
||||
audit("get", args.id, "ok", f"policy={cls}")
|
||||
|
||||
|
||||
def cmd_comment(c: ZentaoClient, args):
|
||||
c.get_bug(args.id)
|
||||
c.add_comment(args.id, args.content)
|
||||
ok({"id": args.id, "action": "comment"})
|
||||
audit("comment", args.id, "ok", args.content[:80])
|
||||
|
||||
|
||||
def cmd_create(c: ZentaoClient, args):
|
||||
payload = {
|
||||
"title": args.title,
|
||||
"steps": args.steps,
|
||||
"severity": args.severity,
|
||||
"pri": args.pri,
|
||||
"type": "codeerror",
|
||||
}
|
||||
if args.assignedTo:
|
||||
payload["assignedTo"] = args.assignedTo
|
||||
if args.module:
|
||||
payload["module"] = args.module
|
||||
r = c.create_bug(payload)
|
||||
new_id = r.get("id") or r.get("bugID")
|
||||
ok({"id": new_id, "action": "create", "raw": r})
|
||||
audit("create", new_id, "ok", args.title[:60])
|
||||
|
||||
|
||||
def cmd_update(c: ZentaoClient, args):
|
||||
bug = c.get_bug(args.id)
|
||||
cls = c.classify(bug)
|
||||
payload = {}
|
||||
if args.status:
|
||||
payload["status"] = args.status
|
||||
if args.resolution:
|
||||
payload["resolution"] = args.resolution
|
||||
if args.assignedTo:
|
||||
payload["assignedTo"] = args.assignedTo
|
||||
|
||||
if cls == "closed":
|
||||
fail(f"铁律C: Bug #{args.id} 状态为 {bug.get('status')},禁止修改")
|
||||
if cls == "human" and (payload.get("status") or payload.get("assignedTo")):
|
||||
fail(f"铁律A: Bug #{args.id} 由人类提出,仅允许 comment;拒绝修改 status/assignedTo")
|
||||
|
||||
c.update_bug(args.id, payload)
|
||||
ok({"id": args.id, "action": "update", "policy": cls, "fields": list(payload.keys())})
|
||||
audit("update", args.id, "ok", f"policy={cls} fields={list(payload.keys())}")
|
||||
|
||||
|
||||
def cmd_list(c: ZentaoClient, args):
|
||||
params = {
|
||||
"status": args.status,
|
||||
"assignedTo": args.assignedTo,
|
||||
"limit": args.limit or 20,
|
||||
"page": 1,
|
||||
}
|
||||
r = c.list_bugs(params)
|
||||
ok({"bugs": r.get("bugs", r), "total": r.get("total", "?")})
|
||||
audit("list", "-", "ok", f"status={args.status}")
|
||||
|
||||
|
||||
def cmd_describe(c: ZentaoClient, args):
|
||||
bug = c.get_bug(args.id)
|
||||
img_dir = SKILL_DIR / "images" / str(args.id)
|
||||
saved = c.download_images(args.id, img_dir)
|
||||
ok({
|
||||
"id": args.id,
|
||||
"title": bug.get("title", ""),
|
||||
"status": bug.get("status", ""),
|
||||
"openedBy": bug.get("openedBy", ""),
|
||||
"policy": c.classify(bug),
|
||||
"image_count": len(saved),
|
||||
"images": [str(p) for p in saved],
|
||||
"steps_plain": (bug.get("steps", "") or "")[:500],
|
||||
"hint": "Use QoderCN Read tool on each image path above to get visual analysis",
|
||||
})
|
||||
audit("describe", args.id, "ok", f"images={len(saved)}")
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser()
|
||||
sub = p.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
sp = sub.add_parser("get"); sp.add_argument("id", type=int)
|
||||
sp = sub.add_parser("comment"); sp.add_argument("id", type=int); sp.add_argument("content")
|
||||
sp = sub.add_parser("create")
|
||||
sp.add_argument("--title", required=True); sp.add_argument("--steps", required=True)
|
||||
sp.add_argument("--severity", type=int, default=2)
|
||||
sp.add_argument("--pri", type=int, default=2)
|
||||
sp.add_argument("--assignedTo"); sp.add_argument("--module", type=int, default=0)
|
||||
|
||||
sp = sub.add_parser("update"); sp.add_argument("id", type=int)
|
||||
sp.add_argument("--status"); sp.add_argument("--resolution"); sp.add_argument("--assignedTo")
|
||||
|
||||
sp = sub.add_parser("list")
|
||||
sp.add_argument("--status", default="active")
|
||||
sp.add_argument("--assignedTo"); sp.add_argument("--limit", type=int, default=20)
|
||||
|
||||
sp = sub.add_parser("describe", help="下载 Bug 截图到本地,供 QoderCN Read 工具识图")
|
||||
sp.add_argument("id", type=int)
|
||||
|
||||
args = p.parse_args()
|
||||
env = load_env()
|
||||
c = ZentaoClient(env)
|
||||
{"get": cmd_get, "comment": cmd_comment, "create": cmd_create,
|
||||
"update": cmd_update, "list": cmd_list, "describe": cmd_describe}[args.cmd](c, args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user