oh-aicoding-tool 0.1.1 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -80
- package/bin/cli.js +257 -383
- package/package.json +28 -56
- package/CODEX_LANGFUSE_PLAN.md +0 -62
- package/bin/langfuse-cli.js +0 -718
- package/codex_langfuse_notify.py +0 -591
- package/langfuse_hook.py +0 -603
- package/opencode-ohai-report/.claude/commands/report-ai-issue.md +0 -60
- package/opencode-ohai-report/.opencode/commands/report-ai-issue.md +0 -30
- package/opencode-ohai-report/.opencode/plugins/oh-ai-report.ts +0 -569
- package/opencode-ohai-report/README.md +0 -45
- package/opencode-ohai-report/bin/cli.js +0 -421
- package/opencode-ohai-report/docs/opencode-ai-issue-collection-architecture.md +0 -313
- package/opencode-ohai-report/docs/opencode-ai-issue-collection-best-practices.md +0 -476
- package/opencode-ohai-report/docs/opencode-ai-issue-collection-phase1-summary.md +0 -405
- package/opencode-ohai-report/examples/issue_output.json +0 -4
- package/opencode-ohai-report/package.json +0 -40
- package/opencode-ohai-report/scripts/claude_report_hook.py +0 -257
- package/opencode-ohai-report/scripts/create_issue.py +0 -34
- package/opencode-ohai-report/scripts/install-claude-plugin.ps1 +0 -254
- package/opencode-ohai-report/scripts/install-opencode-plugin.ps1 +0 -264
- package/opencode-ohai-report/scripts/install-opencode-plugin.sh +0 -218
- package/opencode-ohai-report/scripts/merge-claude-settings.py +0 -99
- package/opencode-ohai-report/tools/ohai-report/README.md +0 -151
- package/opencode-ohai-report/tools/ohai-report/examples/issue-input.json +0 -26
- package/opencode-ohai-report/tools/ohai-report/ohai_report/__init__.py +0 -5
- package/opencode-ohai-report/tools/ohai-report/ohai_report/__main__.py +0 -9
- package/opencode-ohai-report/tools/ohai-report/ohai_report/cli.py +0 -319
- package/opencode-ohai-report/tools/ohai-report/ohai_report/git_context.py +0 -32
- package/opencode-ohai-report/tools/ohai-report/ohai_report/gitcode_defaults.py +0 -14
- package/opencode-ohai-report/tools/ohai-report/ohai_report/issue_markdown.py +0 -313
- package/opencode-ohai-report/tools/ohai-report/ohai_report/metadata.py +0 -360
- package/opencode-ohai-report/tools/ohai-report/ohai_report/observability/__init__.py +0 -1
- package/opencode-ohai-report/tools/ohai-report/ohai_report/observability/langfuse.py +0 -38
- package/opencode-ohai-report/tools/ohai-report/ohai_report/payload.py +0 -64
- package/opencode-ohai-report/tools/ohai-report/ohai_report/schema.py +0 -80
- package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/__init__.py +0 -1
- package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/base.py +0 -15
- package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/gitcode.py +0 -405
- package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/local.py +0 -21
- package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/webhook.py +0 -354
- package/opencode-ohai-report/tools/ohai-report/ohai_report/webhook_defaults.py +0 -9
- package/opencode-ohai-report/tools/ohai-report/ohai_report/workspace.py +0 -61
- package/opencode-ohai-report/tools/ohai-report/ohai_report.py +0 -10
- package/opencode-ohai-report/tools/ohai-report/schemas/report_issue.schema.json +0 -166
- package/scripts/codex-langfuse-check.mjs +0 -101
- package/scripts/codex-langfuse-setup.mjs +0 -181
- package/scripts/langfuse-check.mjs +0 -90
- package/scripts/langfuse-setup.mjs +0 -278
- package/scripts/opencode-langfuse-check.mjs +0 -94
- package/scripts/opencode-langfuse-run.mjs +0 -96
- package/scripts/opencode-langfuse-setup.mjs +0 -478
- package/scripts/resolve-opencode-cli.mjs +0 -58
- package/setup-langfuse.bat +0 -163
- package/setup-langfuse.sh +0 -130
|
@@ -1,405 +0,0 @@
|
|
|
1
|
-
"""GitCode Issue API sink (GitCode OpenAPI v5 create issue)."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
import os
|
|
7
|
-
import urllib.error
|
|
8
|
-
import urllib.parse
|
|
9
|
-
import urllib.request
|
|
10
|
-
from dataclasses import dataclass
|
|
11
|
-
from typing import Any
|
|
12
|
-
|
|
13
|
-
from ..gitcode_defaults import DEFAULT_GITCODE_OWNER, DEFAULT_GITCODE_REPO
|
|
14
|
-
from ..issue_markdown import derive_gitcode_dimension_labels, format_issue_markdown, sanitize_gitcode_label_value
|
|
15
|
-
from .base import SinkResult
|
|
16
|
-
from .webhook import parse_labels_value
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
LABEL_COLOR_BY_NAME = {
|
|
20
|
-
"opencode": "#1d76db",
|
|
21
|
-
"claude": "#5319e7",
|
|
22
|
-
"codex": "#006b75",
|
|
23
|
-
"unspecified": "#ededed",
|
|
24
|
-
"P0": "#d73a4a",
|
|
25
|
-
"P1": "#f66a0a",
|
|
26
|
-
"P2": "#fbca04",
|
|
27
|
-
"P3": "#0e8a16",
|
|
28
|
-
"model-output-error": "#d73a4a",
|
|
29
|
-
"model-capability": "#7057ff",
|
|
30
|
-
"tool-call-failure": "#d93f0b",
|
|
31
|
-
"skill-defect": "#b60205",
|
|
32
|
-
"context-missing": "#c5def5",
|
|
33
|
-
"environment": "#bfdadc",
|
|
34
|
-
"build": "#0052cc",
|
|
35
|
-
"compile": "#0052cc",
|
|
36
|
-
"other": "#ededed",
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
class GitCodeSinkError(RuntimeError):
|
|
41
|
-
def __init__(self, message: str, *, status_code: int = 0, body: str = "") -> None:
|
|
42
|
-
super().__init__(message)
|
|
43
|
-
self.status_code = status_code
|
|
44
|
-
self.body = body
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def _truthy_env(raw: str) -> bool:
|
|
48
|
-
return raw.strip().lower() in ("1", "true", "yes", "on")
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def gitcode_label_color(name: str, fallback: str = "#ededed") -> str:
|
|
52
|
-
label = sanitize_gitcode_label_value(name)
|
|
53
|
-
if label in LABEL_COLOR_BY_NAME:
|
|
54
|
-
return LABEL_COLOR_BY_NAME[label]
|
|
55
|
-
low = label.casefold()
|
|
56
|
-
if low.startswith(("model-", "capability")):
|
|
57
|
-
return "#7057ff"
|
|
58
|
-
if low.startswith(("tool-", "command-", "api-")):
|
|
59
|
-
return "#d93f0b"
|
|
60
|
-
if low.startswith(("env-", "environment")):
|
|
61
|
-
return "#bfdadc"
|
|
62
|
-
return (fallback or "#ededed").strip() or "#ededed"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def merge_gitcode_label_names(payload: dict[str, Any], cfg_labels: str) -> list[str]:
|
|
66
|
-
"""维度标签在前,其次 issue.labels,最后配置/环境附加标签;按大小写不敏感去重。"""
|
|
67
|
-
issue = payload.get("issue") if isinstance(payload.get("issue"), dict) else {}
|
|
68
|
-
merged: list[str] = []
|
|
69
|
-
seen: set[str] = set()
|
|
70
|
-
for group in (
|
|
71
|
-
derive_gitcode_dimension_labels(payload),
|
|
72
|
-
parse_labels_value(issue.get("labels")),
|
|
73
|
-
parse_labels_value(cfg_labels),
|
|
74
|
-
):
|
|
75
|
-
for name in group:
|
|
76
|
-
t = sanitize_gitcode_label_value(name)
|
|
77
|
-
if not t:
|
|
78
|
-
continue
|
|
79
|
-
k = t.casefold()
|
|
80
|
-
if k in seen:
|
|
81
|
-
continue
|
|
82
|
-
seen.add(k)
|
|
83
|
-
merged.append(t)
|
|
84
|
-
return merged
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def _gitcode_request(
|
|
88
|
-
req: urllib.request.Request,
|
|
89
|
-
*,
|
|
90
|
-
timeout: float,
|
|
91
|
-
) -> tuple[int, str]:
|
|
92
|
-
try:
|
|
93
|
-
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
94
|
-
raw = resp.read().decode("utf-8", errors="replace")
|
|
95
|
-
code = int(getattr(resp, "status", 200) or 200)
|
|
96
|
-
return code, raw
|
|
97
|
-
except urllib.error.HTTPError as exc:
|
|
98
|
-
err_body = ""
|
|
99
|
-
try:
|
|
100
|
-
err_body = exc.read().decode("utf-8", errors="replace")
|
|
101
|
-
except OSError:
|
|
102
|
-
pass
|
|
103
|
-
raise GitCodeSinkError(
|
|
104
|
-
f"GitCode 请求失败:HTTP {exc.code}",
|
|
105
|
-
status_code=int(exc.code or 0),
|
|
106
|
-
body=err_body,
|
|
107
|
-
) from exc
|
|
108
|
-
except OSError as exc:
|
|
109
|
-
raise GitCodeSinkError(f"GitCode 请求失败:{exc}") from exc
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def _list_repo_labels_casefold_map(cfg: GitCodeConfig) -> dict[str, str]:
|
|
113
|
-
"""casefold(label_name) -> 仓库中首次出现的规范名称。"""
|
|
114
|
-
api = cfg.api_base.rstrip("/")
|
|
115
|
-
owner = urllib.parse.quote(cfg.owner)
|
|
116
|
-
repo = urllib.parse.quote(cfg.repo)
|
|
117
|
-
out: dict[str, str] = {}
|
|
118
|
-
page = 1
|
|
119
|
-
while page <= 50:
|
|
120
|
-
q = urllib.parse.urlencode(
|
|
121
|
-
{"access_token": cfg.token, "page": str(page), "per_page": "100"},
|
|
122
|
-
)
|
|
123
|
-
url = f"{api}/api/v5/repos/{owner}/{repo}/labels?{q}"
|
|
124
|
-
req = urllib.request.Request(url, method="GET")
|
|
125
|
-
code, raw = _gitcode_request(req, timeout=45.0)
|
|
126
|
-
if code >= 400:
|
|
127
|
-
raise GitCodeSinkError(f"GitCode 获取标签列表失败:HTTP {code}", status_code=code, body=raw[:4096])
|
|
128
|
-
try:
|
|
129
|
-
data = json.loads(raw)
|
|
130
|
-
except json.JSONDecodeError as exc:
|
|
131
|
-
raise GitCodeSinkError("GitCode 标签列表响应不是合法 JSON") from exc
|
|
132
|
-
if isinstance(data, list):
|
|
133
|
-
items = data
|
|
134
|
-
elif isinstance(data, dict):
|
|
135
|
-
items = (
|
|
136
|
-
data.get("list")
|
|
137
|
-
or data.get("data")
|
|
138
|
-
or data.get("labels")
|
|
139
|
-
or data.get("result")
|
|
140
|
-
or []
|
|
141
|
-
)
|
|
142
|
-
if not isinstance(items, list):
|
|
143
|
-
items = []
|
|
144
|
-
else:
|
|
145
|
-
items = []
|
|
146
|
-
if not items:
|
|
147
|
-
break
|
|
148
|
-
for it in items:
|
|
149
|
-
if isinstance(it, dict):
|
|
150
|
-
nm = it.get("name")
|
|
151
|
-
if isinstance(nm, str) and nm.strip():
|
|
152
|
-
name = nm.strip()
|
|
153
|
-
cf = name.casefold()
|
|
154
|
-
if cf not in out:
|
|
155
|
-
out[cf] = name
|
|
156
|
-
if len(items) < 100:
|
|
157
|
-
break
|
|
158
|
-
page += 1
|
|
159
|
-
return out
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def _create_repo_label(cfg: GitCodeConfig, name: str) -> str:
|
|
163
|
-
api = cfg.api_base.rstrip("/")
|
|
164
|
-
owner = urllib.parse.quote(cfg.owner)
|
|
165
|
-
repo = urllib.parse.quote(cfg.repo)
|
|
166
|
-
q = urllib.parse.urlencode({"access_token": cfg.token})
|
|
167
|
-
url = f"{api}/api/v5/repos/{owner}/{repo}/labels?{q}"
|
|
168
|
-
form: dict[str, str] = {"name": name, "color": gitcode_label_color(name, cfg.new_label_color)}
|
|
169
|
-
data = urllib.parse.urlencode(form).encode("utf-8")
|
|
170
|
-
req = urllib.request.Request(url, data=data, method="POST")
|
|
171
|
-
req.add_header("Content-Type", "application/x-www-form-urlencoded")
|
|
172
|
-
req.add_header("Accept", "application/json")
|
|
173
|
-
try:
|
|
174
|
-
code, raw = _gitcode_request(req, timeout=30.0)
|
|
175
|
-
except GitCodeSinkError as exc:
|
|
176
|
-
body_l = (exc.body or "").lower()
|
|
177
|
-
if exc.status_code in (400, 401, 403, 409, 422) and (
|
|
178
|
-
"exist" in body_l or "已存在" in (exc.body or "") or "duplicate" in body_l
|
|
179
|
-
):
|
|
180
|
-
return name
|
|
181
|
-
raise
|
|
182
|
-
if code >= 400:
|
|
183
|
-
raise GitCodeSinkError(
|
|
184
|
-
f"GitCode 新建标签失败:HTTP {code}({name})",
|
|
185
|
-
status_code=code,
|
|
186
|
-
body=raw[:4096],
|
|
187
|
-
)
|
|
188
|
-
try:
|
|
189
|
-
obj = json.loads(raw)
|
|
190
|
-
except json.JSONDecodeError:
|
|
191
|
-
return name
|
|
192
|
-
if isinstance(obj, dict):
|
|
193
|
-
nm = obj.get("name")
|
|
194
|
-
if isinstance(nm, str) and nm.strip():
|
|
195
|
-
return nm.strip()
|
|
196
|
-
return name
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
def resolve_gitcode_issue_labels(cfg: GitCodeConfig, wanted: list[str]) -> list[str]:
|
|
200
|
-
"""按仓库已有标签解析名称;维护者模式下缺失则创建;否则缺失时报错。"""
|
|
201
|
-
if not wanted:
|
|
202
|
-
return []
|
|
203
|
-
by_cf = _list_repo_labels_casefold_map(cfg)
|
|
204
|
-
resolved: list[str] = []
|
|
205
|
-
missing: list[str] = []
|
|
206
|
-
for want in wanted:
|
|
207
|
-
cf = want.casefold()
|
|
208
|
-
if cf in by_cf:
|
|
209
|
-
resolved.append(by_cf[cf])
|
|
210
|
-
continue
|
|
211
|
-
if cfg.label_maintainer:
|
|
212
|
-
created = _create_repo_label(cfg, want)
|
|
213
|
-
cfc = created.casefold()
|
|
214
|
-
if cfc not in by_cf:
|
|
215
|
-
by_cf[cfc] = created
|
|
216
|
-
resolved.append(by_cf[cfc])
|
|
217
|
-
else:
|
|
218
|
-
missing.append(want)
|
|
219
|
-
if missing:
|
|
220
|
-
joined = ", ".join(missing)
|
|
221
|
-
raise GitCodeSinkError(
|
|
222
|
-
"GitCode 仓库中不存在下列标签,且当前未开启「维护者自动建标签」:"
|
|
223
|
-
f"{joined}。"
|
|
224
|
-
"请让仓库管理员预先创建这些标签,或在有 Maintainer 权限的机器人上设置环境变量 "
|
|
225
|
-
"`OHAI_GITCODE_LABEL_MAINTAINER=1`(或 `--gitcode-label-maintainer`)。",
|
|
226
|
-
)
|
|
227
|
-
return resolved
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
def _attach_issue_labels_json(cfg: GitCodeConfig, issue_number: int | str, label_names: list[str]) -> None:
|
|
231
|
-
"""Gitee/GitCode OpenAPI:创建 Issue 时 form 的 labels 常被忽略,须用 JSON 数组单独绑定。"""
|
|
232
|
-
if not label_names:
|
|
233
|
-
return
|
|
234
|
-
num = str(issue_number).strip()
|
|
235
|
-
if not num:
|
|
236
|
-
raise GitCodeSinkError("GitCode 绑定标签失败:创建响应中缺少 Issue number")
|
|
237
|
-
api = cfg.api_base.rstrip("/")
|
|
238
|
-
owner = urllib.parse.quote(cfg.owner)
|
|
239
|
-
repo = urllib.parse.quote(cfg.repo)
|
|
240
|
-
q = urllib.parse.urlencode({"access_token": cfg.token})
|
|
241
|
-
url = f"{api}/api/v5/repos/{owner}/{repo}/issues/{urllib.parse.quote(num, safe='')}/labels?{q}"
|
|
242
|
-
payload = json.dumps(label_names, ensure_ascii=False).encode("utf-8")
|
|
243
|
-
req = urllib.request.Request(url, data=payload, method="POST")
|
|
244
|
-
req.add_header("Content-Type", "application/json; charset=utf-8")
|
|
245
|
-
req.add_header("Accept", "application/json")
|
|
246
|
-
code, raw = _gitcode_request(req, timeout=45.0)
|
|
247
|
-
if code >= 400:
|
|
248
|
-
raise GitCodeSinkError(
|
|
249
|
-
f"GitCode 绑定 Issue 标签失败:HTTP {code}(Issue #{num})",
|
|
250
|
-
status_code=code,
|
|
251
|
-
body=raw[:4096],
|
|
252
|
-
)
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
@dataclass(frozen=True)
|
|
256
|
-
class GitCodeConfig:
|
|
257
|
-
owner: str
|
|
258
|
-
repo: str
|
|
259
|
-
token: str
|
|
260
|
-
api_base: str = "https://api.gitcode.com"
|
|
261
|
-
labels: str = ""
|
|
262
|
-
label_maintainer: bool = False
|
|
263
|
-
new_label_color: str = "#ededed"
|
|
264
|
-
|
|
265
|
-
@classmethod
|
|
266
|
-
def from_env(
|
|
267
|
-
cls,
|
|
268
|
-
*,
|
|
269
|
-
owner: str | None = None,
|
|
270
|
-
repo: str | None = None,
|
|
271
|
-
token: str | None = None,
|
|
272
|
-
api_base: str | None = None,
|
|
273
|
-
labels: str | None = None,
|
|
274
|
-
label_maintainer: bool | None = None,
|
|
275
|
-
new_label_color: str | None = None,
|
|
276
|
-
) -> GitCodeConfig:
|
|
277
|
-
env = os.environ
|
|
278
|
-
o = (owner or env.get("OHAI_GITCODE_OWNER") or env.get("GITCODE_OWNER") or "").strip()
|
|
279
|
-
if not o:
|
|
280
|
-
o = (DEFAULT_GITCODE_OWNER or "").strip()
|
|
281
|
-
r = (repo or env.get("OHAI_GITCODE_REPO") or env.get("GITCODE_REPO") or "").strip()
|
|
282
|
-
if not r:
|
|
283
|
-
r = (DEFAULT_GITCODE_REPO or "").strip()
|
|
284
|
-
t = (token or env.get("OHAI_GITCODE_TOKEN") or env.get("GITCODE_ACCESS_TOKEN") or "").strip()
|
|
285
|
-
base = (
|
|
286
|
-
api_base
|
|
287
|
-
or env.get("OHAI_GITCODE_API_BASE")
|
|
288
|
-
or env.get("GITCODE_API_BASE")
|
|
289
|
-
or "https://api.gitcode.com"
|
|
290
|
-
).strip().rstrip("/")
|
|
291
|
-
lbl = labels if labels is not None else env.get("OHAI_GITCODE_LABELS", "") or ""
|
|
292
|
-
if label_maintainer is not None:
|
|
293
|
-
maint = bool(label_maintainer)
|
|
294
|
-
else:
|
|
295
|
-
maint = _truthy_env(env.get("OHAI_GITCODE_LABEL_MAINTAINER", "")) or _truthy_env(
|
|
296
|
-
env.get("OHAI_GITCODE_MAINTAINER", ""),
|
|
297
|
-
)
|
|
298
|
-
color_raw = (
|
|
299
|
-
(new_label_color or "").strip()
|
|
300
|
-
if new_label_color is not None
|
|
301
|
-
else (env.get("OHAI_GITCODE_NEW_LABEL_COLOR") or "").strip()
|
|
302
|
-
)
|
|
303
|
-
color = color_raw or "#ededed"
|
|
304
|
-
if not o or not r or not t:
|
|
305
|
-
raise GitCodeSinkError(
|
|
306
|
-
"GitCode 缺少配置:请设置 Token 环境变量 OHAI_GITCODE_TOKEN(或 GITCODE_ACCESS_TOKEN);"
|
|
307
|
-
"命名空间与仓库名请设置 OHAI_GITCODE_OWNER / OHAI_GITCODE_REPO,"
|
|
308
|
-
"或在 ohai_report/gitcode_defaults.py 中填写 DEFAULT_GITCODE_OWNER / DEFAULT_GITCODE_REPO。"
|
|
309
|
-
)
|
|
310
|
-
return cls(
|
|
311
|
-
owner=o,
|
|
312
|
-
repo=r,
|
|
313
|
-
token=t,
|
|
314
|
-
api_base=base,
|
|
315
|
-
labels=lbl.strip(),
|
|
316
|
-
label_maintainer=maint,
|
|
317
|
-
new_label_color=color,
|
|
318
|
-
)
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
class GitCodeIssueSink:
|
|
322
|
-
def __init__(self, config: GitCodeConfig) -> None:
|
|
323
|
-
self.config = config
|
|
324
|
-
|
|
325
|
-
def save(self, payload: dict[str, Any]) -> SinkResult:
|
|
326
|
-
issue = payload.get("issue") if isinstance(payload.get("issue"), dict) else {}
|
|
327
|
-
title = str(issue.get("title") or "").strip()
|
|
328
|
-
if not title:
|
|
329
|
-
raise GitCodeSinkError("issue.title 不能为空")
|
|
330
|
-
|
|
331
|
-
api = self.config.api_base.rstrip("/")
|
|
332
|
-
endpoint = f"{api}/api/v5/repos/{urllib.parse.quote(self.config.owner)}/issues"
|
|
333
|
-
q = urllib.parse.urlencode({"access_token": self.config.token})
|
|
334
|
-
|
|
335
|
-
body = format_issue_markdown(payload)
|
|
336
|
-
form: dict[str, str] = {
|
|
337
|
-
"repo": self.config.repo,
|
|
338
|
-
"title": title,
|
|
339
|
-
"body": body,
|
|
340
|
-
}
|
|
341
|
-
merged = merge_gitcode_label_names(payload, self.config.labels)
|
|
342
|
-
bound: list[str] = []
|
|
343
|
-
if merged:
|
|
344
|
-
bound = resolve_gitcode_issue_labels(self.config, merged)
|
|
345
|
-
|
|
346
|
-
data = urllib.parse.urlencode(form).encode("utf-8")
|
|
347
|
-
req = urllib.request.Request(f"{endpoint}?{q}", data=data, method="POST")
|
|
348
|
-
req.add_header("Content-Type", "application/x-www-form-urlencoded")
|
|
349
|
-
req.add_header("Accept", "application/json")
|
|
350
|
-
|
|
351
|
-
try:
|
|
352
|
-
with urllib.request.urlopen(req, timeout=60) as resp:
|
|
353
|
-
raw = resp.read().decode("utf-8", errors="replace")
|
|
354
|
-
code = getattr(resp, "status", 200)
|
|
355
|
-
except urllib.error.HTTPError as exc:
|
|
356
|
-
err_body = ""
|
|
357
|
-
try:
|
|
358
|
-
err_body = exc.read().decode("utf-8", errors="replace")
|
|
359
|
-
except OSError:
|
|
360
|
-
pass
|
|
361
|
-
raise GitCodeSinkError(
|
|
362
|
-
f"GitCode 创建 Issue 失败:HTTP {exc.code}",
|
|
363
|
-
status_code=int(exc.code or 0),
|
|
364
|
-
body=err_body,
|
|
365
|
-
) from exc
|
|
366
|
-
except OSError as exc:
|
|
367
|
-
raise GitCodeSinkError(f"GitCode 请求失败:{exc}") from exc
|
|
368
|
-
|
|
369
|
-
try:
|
|
370
|
-
obj = json.loads(raw)
|
|
371
|
-
except json.JSONDecodeError as exc:
|
|
372
|
-
raise GitCodeSinkError(f"GitCode 响应不是合法 JSON(HTTP {code})") from exc
|
|
373
|
-
|
|
374
|
-
if not isinstance(obj, dict):
|
|
375
|
-
raise GitCodeSinkError(f"GitCode 响应格式异常(HTTP {code})")
|
|
376
|
-
|
|
377
|
-
html_url = str(obj.get("html_url") or "")
|
|
378
|
-
number = obj.get("number")
|
|
379
|
-
if bound:
|
|
380
|
-
if number is None:
|
|
381
|
-
raise GitCodeSinkError(
|
|
382
|
-
"GitCode 已创建 Issue 但响应未包含 number,无法绑定标签;请检查 API 或联系平台支持。",
|
|
383
|
-
)
|
|
384
|
-
_attach_issue_labels_json(self.config, number, bound)
|
|
385
|
-
|
|
386
|
-
return SinkResult(
|
|
387
|
-
issue_id=str(payload.get("issue_id") or ""),
|
|
388
|
-
path=None,
|
|
389
|
-
url=html_url,
|
|
390
|
-
extra={"number": number, "id": obj.get("id")},
|
|
391
|
-
)
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
def apply_gitcode_result(payload: dict[str, Any], result: SinkResult) -> dict[str, Any]:
|
|
395
|
-
"""Mutate payload with GitCode remote refs and matching status."""
|
|
396
|
-
out = dict(payload)
|
|
397
|
-
extra = getattr(result, "extra", {}) or {}
|
|
398
|
-
num = extra.get("number")
|
|
399
|
-
gid = extra.get("id")
|
|
400
|
-
gc_block: dict[str, Any] = {"html_url": result.url, "number": num}
|
|
401
|
-
if gid is not None:
|
|
402
|
-
gc_block["id"] = gid
|
|
403
|
-
out["gitcode"] = gc_block
|
|
404
|
-
out["status"] = "created-gitcode"
|
|
405
|
-
return out
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
"""Local JSON file sink."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import pathlib
|
|
6
|
-
from typing import Any
|
|
7
|
-
|
|
8
|
-
from .base import SinkResult
|
|
9
|
-
from ..workspace import data_dir, write_json
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class LocalIssueSink:
|
|
13
|
-
def __init__(self, root: pathlib.Path) -> None:
|
|
14
|
-
self.root = root
|
|
15
|
-
|
|
16
|
-
def save(self, payload: dict[str, Any]) -> SinkResult:
|
|
17
|
-
root_dir = data_dir(self.root)
|
|
18
|
-
issue_path = root_dir / "issues" / f"{payload['issue_id']}.json"
|
|
19
|
-
write_json(issue_path, payload)
|
|
20
|
-
write_json(root_dir / "latest.json", payload)
|
|
21
|
-
return SinkResult(issue_id=payload["issue_id"], path=issue_path)
|