@d2c2d/crikket-sdk 1.0.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.
- package/README.md +226 -0
- package/dist/chunk-VBFPP5MD.js +71 -0
- package/dist/index.d.ts +108 -0
- package/dist/index.js +527 -0
- package/dist/rpc.d.ts +11 -0
- package/dist/rpc.js +10 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# @d2c2d/crikket-sdk
|
|
2
|
+
|
|
3
|
+
Crikket 缺陷平台匿名 oRPC 客户端(Node.js ≥ 18,ESM)。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @d2c2d/crikket-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 使用
|
|
12
|
+
|
|
13
|
+
SDK 不读取 env 或配置文件;在脚本中自行读取 JSON 并传入参数。
|
|
14
|
+
|
|
15
|
+
### 配置 `crikket.config.json`
|
|
16
|
+
|
|
17
|
+
| 字段 | 说明 |
|
|
18
|
+
|------|------|
|
|
19
|
+
| `rpcBaseUrl` | Crikket RPC 根 URL,必填 |
|
|
20
|
+
| `organizationId` | 组织 ID;`list` / `export` / `mark` 等方法需要 |
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"rpcBaseUrl": "http://localhost:9100",
|
|
25
|
+
"organizationId": "org_xxx"
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 按 id 下载 — `download-bugs.mjs`
|
|
30
|
+
|
|
31
|
+
```javascript
|
|
32
|
+
// 用法: node download-bugs.mjs <bugId> [bugId...]
|
|
33
|
+
// 示例: node download-bugs.mjs bug_1 bug_2
|
|
34
|
+
// 说明: 匿名下载,无需 organizationId;输出到 ./out-bugs/{id}.md 等
|
|
35
|
+
|
|
36
|
+
import { mkdirSync, readFileSync } from "node:fs";
|
|
37
|
+
import { CrikketClient } from "@d2c2d/crikket-sdk";
|
|
38
|
+
|
|
39
|
+
const cfg = JSON.parse(readFileSync("./crikket.config.json", "utf8"));
|
|
40
|
+
const ids = process.argv.slice(2).filter((s) => s.trim());
|
|
41
|
+
if (ids.length === 0) {
|
|
42
|
+
console.error("用法: node download-bugs.mjs <bugId> [bugId...]");
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const outDir = "./out-bugs";
|
|
47
|
+
mkdirSync(outDir, { recursive: true });
|
|
48
|
+
|
|
49
|
+
const client = new CrikketClient({ baseUrl: cfg.rpcBaseUrl });
|
|
50
|
+
const results = await client.downloadBugs({ ids, outDir });
|
|
51
|
+
|
|
52
|
+
for (const row of results) {
|
|
53
|
+
console.log(row.ok ? `✓ ${row.id}` : `✗ ${row.id}: ${row.error}`);
|
|
54
|
+
}
|
|
55
|
+
if (results.some((r) => !r.ok)) process.exit(1);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 导出 open bug — `export-open-bugs.mjs`
|
|
59
|
+
|
|
60
|
+
```javascript
|
|
61
|
+
// 用法: node export-open-bugs.mjs <search> [outDir]
|
|
62
|
+
// 示例: node export-open-bugs.mjs http://localhost:3003 ./out-export
|
|
63
|
+
// 说明: 按 search 筛选 open bug 并下载;实际写入 {outDir}/{search slug}/
|
|
64
|
+
|
|
65
|
+
import { mkdirSync, readFileSync } from "node:fs";
|
|
66
|
+
import { CrikketClient } from "@d2c2d/crikket-sdk";
|
|
67
|
+
|
|
68
|
+
const cfg = JSON.parse(readFileSync("./crikket.config.json", "utf8"));
|
|
69
|
+
const search = process.argv[2]?.trim();
|
|
70
|
+
const outDir = process.argv[3]?.trim() ?? "./out-export";
|
|
71
|
+
if (!search) {
|
|
72
|
+
console.error("用法: node export-open-bugs.mjs <search> [outDir]");
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
mkdirSync(outDir, { recursive: true });
|
|
77
|
+
|
|
78
|
+
const client = new CrikketClient({
|
|
79
|
+
baseUrl: cfg.rpcBaseUrl,
|
|
80
|
+
organizationId: cfg.organizationId, // export 需要 org
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const { ids, outDir: targetDir, downloadResults } = await client.exportOpenBugs({
|
|
84
|
+
search,
|
|
85
|
+
outDir,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
console.log(`open=${ids.length} → ${targetDir}`);
|
|
89
|
+
for (const row of downloadResults) {
|
|
90
|
+
console.log(row.ok ? `✓ ${row.id}` : `✗ ${row.id}: ${row.error}`);
|
|
91
|
+
}
|
|
92
|
+
if (downloadResults.some((r) => !r.ok)) process.exit(1);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 批量 mark resolved — `mark-resolved.mjs`
|
|
96
|
+
|
|
97
|
+
```javascript
|
|
98
|
+
// 用法: node mark-resolved.mjs <bugId> [bugId...]
|
|
99
|
+
// 示例: node mark-resolved.mjs bug_1 bug_2
|
|
100
|
+
// 说明: 批量将 status 改为 resolved;markInProgress / markOpen 换方法名即可
|
|
101
|
+
|
|
102
|
+
import { readFileSync } from "node:fs";
|
|
103
|
+
import { CrikketClient } from "@d2c2d/crikket-sdk";
|
|
104
|
+
|
|
105
|
+
const cfg = JSON.parse(readFileSync("./crikket.config.json", "utf8"));
|
|
106
|
+
const ids = process.argv.slice(2).filter((s) => s.trim());
|
|
107
|
+
if (ids.length === 0) {
|
|
108
|
+
console.error("用法: node mark-resolved.mjs <bugId> [bugId...]");
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const client = new CrikketClient({
|
|
113
|
+
baseUrl: cfg.rpcBaseUrl,
|
|
114
|
+
organizationId: cfg.organizationId, // bulk 改 status 需要 org
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const result = await client.markResolved({ ids });
|
|
118
|
+
console.log(`updated ${result.updatedCount} bug(s): ${result.ids?.join(", ") ?? ""}`);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## API
|
|
122
|
+
|
|
123
|
+
### `CrikketClient`
|
|
124
|
+
|
|
125
|
+
**构造**
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
new CrikketClient(opts: {
|
|
129
|
+
baseUrl: string; // 非空
|
|
130
|
+
organizationId?: string;
|
|
131
|
+
fetch?: typeof fetch;
|
|
132
|
+
})
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**方法**
|
|
136
|
+
|
|
137
|
+
| 方法 | 说明 | `organizationId` |
|
|
138
|
+
|------|------|------------------|
|
|
139
|
+
| `getBugById(id)` | 单条 bug 详情 | 否 |
|
|
140
|
+
| `getDebuggerEvents(id)` | 调试事件 | 否 |
|
|
141
|
+
| `downloadBugs({ ids, outDir })` | 下载 Markdown + sidecar | 否 |
|
|
142
|
+
| `listByOrganizationId(input?)` | 分页列表 | 构造或 `input.organizationId` |
|
|
143
|
+
| `getDashboardStats(input?)` | 仪表盘统计 | 构造或 `input.organizationId` |
|
|
144
|
+
| `exportOpenBugs({ search, outDir, organizationId? })` | 列出 open 并下载 | 构造或参数 |
|
|
145
|
+
| `updateBulkStatus({ ids, status, organizationId? })` | 通用批量改 status | 构造或参数 |
|
|
146
|
+
| `markInProgress({ ids, organizationId? })` | → `in_progress` | 构造或参数 |
|
|
147
|
+
| `markResolved({ ids, organizationId? })` | → `resolved` | 构造或参数 |
|
|
148
|
+
| `markOpen({ ids, organizationId? })` | → `open` | 构造或参数 |
|
|
149
|
+
|
|
150
|
+
缺少 `organizationId` 时,需要 org 的方法会在发请求前抛出错误。SDK 不校验 bug 是否属于该 org。
|
|
151
|
+
|
|
152
|
+
**`listByOrganizationId` 参数**
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
{
|
|
156
|
+
organizationId?: string;
|
|
157
|
+
page?: number; // 默认 1
|
|
158
|
+
perPage?: number; // 默认 12
|
|
159
|
+
search?: string;
|
|
160
|
+
sort?: string; // 默认 "newest"
|
|
161
|
+
statuses?: BugStatus[];
|
|
162
|
+
priorities?: string[];
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**`downloadBugs` 返回值** — `DownloadResult[]`:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
{
|
|
170
|
+
id: string;
|
|
171
|
+
ok: boolean;
|
|
172
|
+
error?: string;
|
|
173
|
+
paths?: {
|
|
174
|
+
md: string;
|
|
175
|
+
reportJson: string;
|
|
176
|
+
debuggerJson: string;
|
|
177
|
+
networkJson?: string;
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**`exportOpenBugs` 返回值** — `ExportOpenResult`:
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
{
|
|
186
|
+
ids: string[];
|
|
187
|
+
outDir: string; // 实际写入目录(含 search slug)
|
|
188
|
+
downloadResults: DownloadResult[];
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### 主入口辅助函数
|
|
193
|
+
|
|
194
|
+
| 导出 | 说明 |
|
|
195
|
+
|------|------|
|
|
196
|
+
| `uniqueBugIds(ids)` | trim 并去重 |
|
|
197
|
+
| `validateBugIdList(ids)` | 校验非空、≤200 个 |
|
|
198
|
+
| `assertBulkUpdatedCount(result, expected)` | 校验 `updatedCount` |
|
|
199
|
+
| `updateBulkStatus({ baseUrl, organizationId, bugIds, status, fetchImpl? })` | 无 client 实例的 bulk 调用 |
|
|
200
|
+
| `searchNormalized(search)` | 将 search 转为安全目录名 slug |
|
|
201
|
+
|
|
202
|
+
### `@d2c2d/crikket-sdk/rpc`
|
|
203
|
+
|
|
204
|
+
| 导出 | 说明 |
|
|
205
|
+
|------|------|
|
|
206
|
+
| `rpcPost(baseUrl, procedurePath, jsonInput, fetchImpl?)` | 原始 oRPC POST |
|
|
207
|
+
| `unwrapProcedureResult(body)` | 解析 tRPC/oRPC 响应体 |
|
|
208
|
+
| `RpcError` | HTTP 或 RPC 错误 |
|
|
209
|
+
|
|
210
|
+
### 类型
|
|
211
|
+
|
|
212
|
+
`BugStatus`、`BulkUpdateResult`、`CrikketClientOptions`、`DashboardStatsInput`、`DownloadResult`、`ExportOpenResult`、`ListByOrganizationIdInput`、`ListPage`
|
|
213
|
+
|
|
214
|
+
## 输出目录
|
|
215
|
+
|
|
216
|
+
**`downloadBugs`** — 扁平写入 `{outDir}/`:
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
{id}.md
|
|
220
|
+
{id}.report.json
|
|
221
|
+
{id}.debugger.json
|
|
222
|
+
{id}.network.json # 有 networkRequests 时
|
|
223
|
+
{id}.attachments/*.picked_dom.html
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**`exportOpenBugs`** — 写入 `{outDir}/{searchNormalized(search)}/`,文件名同上。
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// src/rpc.ts
|
|
2
|
+
function unwrapProcedureResult(body) {
|
|
3
|
+
if (body === null || body === void 0) return body;
|
|
4
|
+
if (typeof body !== "object") return body;
|
|
5
|
+
const rec = body;
|
|
6
|
+
if ("json" in rec && rec.json !== void 0) {
|
|
7
|
+
return rec.json;
|
|
8
|
+
}
|
|
9
|
+
const nested = rec.result;
|
|
10
|
+
if (nested && typeof nested === "object") {
|
|
11
|
+
const r = nested;
|
|
12
|
+
const data = r.data;
|
|
13
|
+
if (data && typeof data === "object") {
|
|
14
|
+
const d = data;
|
|
15
|
+
if ("json" in d) return d.json;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (Array.isArray(body) && body.length > 0) {
|
|
19
|
+
return unwrapProcedureResult(body[0]);
|
|
20
|
+
}
|
|
21
|
+
return body;
|
|
22
|
+
}
|
|
23
|
+
var RpcError = class extends Error {
|
|
24
|
+
status;
|
|
25
|
+
statusText;
|
|
26
|
+
bodyPreview;
|
|
27
|
+
constructor(status, statusText, bodyPreview) {
|
|
28
|
+
super(`RPC ${status} ${statusText}
|
|
29
|
+
${bodyPreview}`);
|
|
30
|
+
this.name = "RpcError";
|
|
31
|
+
this.status = status;
|
|
32
|
+
this.statusText = statusText;
|
|
33
|
+
this.bodyPreview = bodyPreview;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
async function rpcPost(rpcBaseUrl, procedurePath, jsonInput, fetchImpl) {
|
|
37
|
+
const base = rpcBaseUrl.replace(/\/+$/, "");
|
|
38
|
+
const url = `${base}/rpc/${procedurePath}`;
|
|
39
|
+
const doFetch = fetchImpl ?? globalThis.fetch;
|
|
40
|
+
const res = await doFetch(url, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: {
|
|
43
|
+
Accept: "*/*",
|
|
44
|
+
"content-type": "application/json"
|
|
45
|
+
},
|
|
46
|
+
body: JSON.stringify({ json: jsonInput, meta: [] })
|
|
47
|
+
});
|
|
48
|
+
const text = await res.text();
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = text ? JSON.parse(text) : null;
|
|
52
|
+
} catch {
|
|
53
|
+
throw new RpcError(res.status, res.statusText, text.slice(0, 2e3));
|
|
54
|
+
}
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
const preview = typeof text === "string" ? text.slice(0, 2e3) : String(parsed).slice(0, 2e3);
|
|
57
|
+
throw new RpcError(res.status, res.statusText, preview);
|
|
58
|
+
}
|
|
59
|
+
if (parsed && typeof parsed === "object" && "error" in parsed) {
|
|
60
|
+
const err = parsed.error;
|
|
61
|
+
const msg = err && typeof err === "object" && err !== null && "message" in err ? String(err.message) : JSON.stringify(err);
|
|
62
|
+
throw new RpcError(res.status, "tRPC error", msg.slice(0, 2e3));
|
|
63
|
+
}
|
|
64
|
+
return unwrapProcedureResult(parsed);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export {
|
|
68
|
+
unwrapProcedureResult,
|
|
69
|
+
RpcError,
|
|
70
|
+
rpcPost
|
|
71
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export { RpcError, rpcPost, unwrapProcedureResult } from './rpc.js';
|
|
2
|
+
|
|
3
|
+
type BugStatus = "open" | "in_progress" | "resolved" | "closed";
|
|
4
|
+
type BulkUpdateResult = {
|
|
5
|
+
updatedCount?: number;
|
|
6
|
+
ids?: string[];
|
|
7
|
+
};
|
|
8
|
+
type ListByOrganizationIdInput = {
|
|
9
|
+
organizationId?: string;
|
|
10
|
+
page?: number;
|
|
11
|
+
perPage?: number;
|
|
12
|
+
search?: string;
|
|
13
|
+
sort?: string;
|
|
14
|
+
statuses?: BugStatus[];
|
|
15
|
+
priorities?: string[];
|
|
16
|
+
};
|
|
17
|
+
type ListPage = Record<string, unknown> & {
|
|
18
|
+
items?: unknown[];
|
|
19
|
+
pagination?: Record<string, unknown>;
|
|
20
|
+
};
|
|
21
|
+
type DashboardStatsInput = {
|
|
22
|
+
organizationId?: string;
|
|
23
|
+
search?: string;
|
|
24
|
+
statuses?: BugStatus[];
|
|
25
|
+
priorities?: string[];
|
|
26
|
+
};
|
|
27
|
+
type DownloadResult = {
|
|
28
|
+
id: string;
|
|
29
|
+
ok: boolean;
|
|
30
|
+
error?: string;
|
|
31
|
+
paths?: {
|
|
32
|
+
md: string;
|
|
33
|
+
reportJson: string;
|
|
34
|
+
debuggerJson: string;
|
|
35
|
+
networkJson?: string;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
type ExportOpenResult = {
|
|
39
|
+
ids: string[];
|
|
40
|
+
outDir: string;
|
|
41
|
+
downloadResults: DownloadResult[];
|
|
42
|
+
};
|
|
43
|
+
type CrikketClientOptions = {
|
|
44
|
+
baseUrl: string;
|
|
45
|
+
organizationId?: string;
|
|
46
|
+
fetch?: typeof fetch;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
declare class CrikketClient {
|
|
50
|
+
private readonly baseUrl;
|
|
51
|
+
private readonly organizationId?;
|
|
52
|
+
private readonly fetchImpl;
|
|
53
|
+
constructor(opts: CrikketClientOptions);
|
|
54
|
+
private resolveOrganizationId;
|
|
55
|
+
private rpc;
|
|
56
|
+
listByOrganizationId(input: ListByOrganizationIdInput): Promise<ListPage>;
|
|
57
|
+
getDashboardStats(input?: DashboardStatsInput): Promise<unknown>;
|
|
58
|
+
getBugById(id: string): Promise<unknown>;
|
|
59
|
+
getDebuggerEvents(id: string): Promise<unknown>;
|
|
60
|
+
updateBulkStatus(input: {
|
|
61
|
+
organizationId?: string;
|
|
62
|
+
ids: string[];
|
|
63
|
+
status: BugStatus;
|
|
64
|
+
}): Promise<BulkUpdateResult>;
|
|
65
|
+
markInProgress(input: {
|
|
66
|
+
ids: string[];
|
|
67
|
+
organizationId?: string;
|
|
68
|
+
}): Promise<BulkUpdateResult>;
|
|
69
|
+
markResolved(input: {
|
|
70
|
+
ids: string[];
|
|
71
|
+
organizationId?: string;
|
|
72
|
+
}): Promise<BulkUpdateResult>;
|
|
73
|
+
markOpen(input: {
|
|
74
|
+
ids: string[];
|
|
75
|
+
organizationId?: string;
|
|
76
|
+
}): Promise<BulkUpdateResult>;
|
|
77
|
+
downloadBugs(input: {
|
|
78
|
+
ids: string[];
|
|
79
|
+
outDir: string;
|
|
80
|
+
}): Promise<DownloadResult[]>;
|
|
81
|
+
exportOpenBugs(input: {
|
|
82
|
+
search: string;
|
|
83
|
+
outDir: string;
|
|
84
|
+
organizationId?: string;
|
|
85
|
+
}): Promise<ExportOpenResult>;
|
|
86
|
+
private listAllOpenIds;
|
|
87
|
+
private downloadOneBug;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
declare function uniqueBugIds(ids: string[]): string[];
|
|
91
|
+
declare function validateBugIdList(bugIds: string[]): string[];
|
|
92
|
+
declare function assertBulkUpdatedCount(result: BulkUpdateResult, expected: number): BulkUpdateResult;
|
|
93
|
+
declare function updateBulkStatus(args: {
|
|
94
|
+
baseUrl: string;
|
|
95
|
+
organizationId: string;
|
|
96
|
+
bugIds: string[];
|
|
97
|
+
status: BugStatus;
|
|
98
|
+
fetchImpl?: typeof fetch;
|
|
99
|
+
}): Promise<BulkUpdateResult>;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 将 trim 后的 search 规范为安全目录名(单表、可文档化)。
|
|
103
|
+
* 规则:若能解析为 URL,则 `{scheme}_{hostname}_{port?}`,端口仅在非 80/443 时追加;
|
|
104
|
+
* 否则对字符串做安全字符替换。
|
|
105
|
+
*/
|
|
106
|
+
declare function searchNormalized(searchTrimmed: string): string;
|
|
107
|
+
|
|
108
|
+
export { type BugStatus, type BulkUpdateResult, CrikketClient, type CrikketClientOptions, type DashboardStatsInput, type DownloadResult, type ExportOpenResult, type ListByOrganizationIdInput, type ListPage, assertBulkUpdatedCount, searchNormalized, uniqueBugIds, updateBulkStatus, validateBugIdList };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RpcError,
|
|
3
|
+
rpcPost,
|
|
4
|
+
unwrapProcedureResult
|
|
5
|
+
} from "./chunk-VBFPP5MD.js";
|
|
6
|
+
|
|
7
|
+
// src/client.ts
|
|
8
|
+
import { mkdirSync as mkdirSync3 } from "fs";
|
|
9
|
+
import { join as join3 } from "path";
|
|
10
|
+
|
|
11
|
+
// src/bulk.ts
|
|
12
|
+
function uniqueBugIds(ids) {
|
|
13
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14
|
+
const out = [];
|
|
15
|
+
for (const raw of ids) {
|
|
16
|
+
const id = raw.trim();
|
|
17
|
+
if (!id) continue;
|
|
18
|
+
if (seen.has(id)) continue;
|
|
19
|
+
seen.add(id);
|
|
20
|
+
out.push(id);
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
function validateBugIdList(bugIds) {
|
|
25
|
+
const unique = uniqueBugIds(bugIds);
|
|
26
|
+
if (unique.length === 0) {
|
|
27
|
+
throw new Error("\u81F3\u5C11\u9700\u8981\u4E00\u4E2A bug id");
|
|
28
|
+
}
|
|
29
|
+
if (unique.length > 200) {
|
|
30
|
+
throw new Error(`bug id \u6700\u591A 200 \u4E2A\uFF08\u5F53\u524D ${unique.length}\uFF09`);
|
|
31
|
+
}
|
|
32
|
+
return unique;
|
|
33
|
+
}
|
|
34
|
+
function assertBulkUpdatedCount(result, expected) {
|
|
35
|
+
const updatedCount = typeof result.updatedCount === "number" ? result.updatedCount : Number.NaN;
|
|
36
|
+
if (updatedCount !== expected) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`updatedCount=${String(result.updatedCount)}\uFF0C\u671F\u671B ${expected}\uFF1B\u54CD\u5E94: ${JSON.stringify(result).slice(0, 500)}`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
async function updateBulkStatus(args) {
|
|
44
|
+
const bugIds = validateBugIdList(args.bugIds);
|
|
45
|
+
let raw;
|
|
46
|
+
try {
|
|
47
|
+
raw = await rpcPost(
|
|
48
|
+
args.baseUrl,
|
|
49
|
+
"bugReport/updateBulkByOrganizationId",
|
|
50
|
+
{
|
|
51
|
+
organizationId: args.organizationId,
|
|
52
|
+
ids: bugIds,
|
|
53
|
+
status: args.status
|
|
54
|
+
},
|
|
55
|
+
args.fetchImpl
|
|
56
|
+
);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
if (e instanceof RpcError) {
|
|
59
|
+
throw new Error(e.message);
|
|
60
|
+
}
|
|
61
|
+
throw e;
|
|
62
|
+
}
|
|
63
|
+
if (!raw || typeof raw !== "object") {
|
|
64
|
+
throw new Error(`bulk \u8FD4\u56DE\u975E\u5BF9\u8C61: ${JSON.stringify(raw).slice(0, 500)}`);
|
|
65
|
+
}
|
|
66
|
+
return assertBulkUpdatedCount(raw, bugIds.length);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/export/attachments.ts
|
|
70
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
71
|
+
import { join } from "path";
|
|
72
|
+
function isPickedDomHtmlAttachment(att) {
|
|
73
|
+
if (!att || typeof att !== "object") return false;
|
|
74
|
+
const o = att;
|
|
75
|
+
if (o.kind !== "picked_dom") return false;
|
|
76
|
+
const ct = typeof o.contentType === "string" ? o.contentType : "";
|
|
77
|
+
return ct.toLowerCase().includes("html");
|
|
78
|
+
}
|
|
79
|
+
function listPickedDomHtmlAttachments(report) {
|
|
80
|
+
if (!report || typeof report !== "object") return [];
|
|
81
|
+
const attachments = report.attachments;
|
|
82
|
+
if (!Array.isArray(attachments)) return [];
|
|
83
|
+
return attachments.filter(isPickedDomHtmlAttachment).map((a) => a).sort((a, b) => {
|
|
84
|
+
const ao = typeof a.sortOrder === "number" ? a.sortOrder : 0;
|
|
85
|
+
const bo = typeof b.sortOrder === "number" ? b.sortOrder : 0;
|
|
86
|
+
return ao - bo;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function pickedDomFileName(attachmentId) {
|
|
90
|
+
return `${attachmentId}.picked_dom.html`;
|
|
91
|
+
}
|
|
92
|
+
function pickedDomRelPath(bugId, attachmentId) {
|
|
93
|
+
return `./${bugId}.attachments/${pickedDomFileName(attachmentId)}`;
|
|
94
|
+
}
|
|
95
|
+
async function downloadPickedDomAttachments(report, outDir, bugId, fetchImpl) {
|
|
96
|
+
const picked = listPickedDomHtmlAttachments(report);
|
|
97
|
+
if (picked.length === 0) return [];
|
|
98
|
+
const attachDir = join(outDir, `${bugId}.attachments`);
|
|
99
|
+
mkdirSync(attachDir, { recursive: true });
|
|
100
|
+
const doFetch = fetchImpl ?? globalThis.fetch;
|
|
101
|
+
const rows = [];
|
|
102
|
+
for (const att of picked) {
|
|
103
|
+
const attachmentId = typeof att.id === "string" ? att.id : "";
|
|
104
|
+
const sortOrder = typeof att.sortOrder === "number" ? att.sortOrder : 0;
|
|
105
|
+
const contentType = typeof att.contentType === "string" ? att.contentType : "";
|
|
106
|
+
const sizeBytes = typeof att.sizeBytes === "number" ? att.sizeBytes : 0;
|
|
107
|
+
const url = typeof att.url === "string" ? att.url : "";
|
|
108
|
+
const base = {
|
|
109
|
+
id: attachmentId,
|
|
110
|
+
sortOrder,
|
|
111
|
+
contentType,
|
|
112
|
+
sizeBytes,
|
|
113
|
+
localRel: null
|
|
114
|
+
};
|
|
115
|
+
if (!attachmentId || !url) {
|
|
116
|
+
const msg = "missing attachment id or url";
|
|
117
|
+
console.error(`[crikket-sdk] ${bugId} picked_dom: ${msg}`);
|
|
118
|
+
rows.push({ ...base, downloadError: msg });
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const filePath = join(attachDir, pickedDomFileName(attachmentId));
|
|
122
|
+
const localRel = pickedDomRelPath(bugId, attachmentId);
|
|
123
|
+
try {
|
|
124
|
+
const res = await doFetch(url);
|
|
125
|
+
if (!res.ok) {
|
|
126
|
+
throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
127
|
+
}
|
|
128
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
129
|
+
writeFileSync(filePath, buf);
|
|
130
|
+
rows.push({ ...base, localRel });
|
|
131
|
+
} catch (e) {
|
|
132
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
133
|
+
console.error(`[crikket-sdk] ${bugId} picked_dom ${attachmentId}: ${msg}`);
|
|
134
|
+
rows.push({ ...base, downloadError: msg });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return rows;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/export/normalizeSearch.ts
|
|
141
|
+
function searchNormalized(searchTrimmed) {
|
|
142
|
+
if (!searchTrimmed) return "empty_search";
|
|
143
|
+
let urlString = searchTrimmed;
|
|
144
|
+
if (!/^[a-zA-Z][a-zA-Z+\-.]*:\/\//.test(urlString)) {
|
|
145
|
+
urlString = `http://${urlString}`;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const u = new URL(urlString);
|
|
149
|
+
const scheme = u.protocol.replace(/:$/, "") || "http";
|
|
150
|
+
const host = u.hostname || "unknown";
|
|
151
|
+
const port = u.port;
|
|
152
|
+
const defaultPort = scheme === "https" ? "443" : scheme === "http" ? "80" : "";
|
|
153
|
+
const includePort = port && port !== defaultPort;
|
|
154
|
+
const raw = includePort ? `${scheme}_${host}_${port}` : `${scheme}_${host}`;
|
|
155
|
+
return sanitizeDirName(raw);
|
|
156
|
+
} catch {
|
|
157
|
+
return sanitizeDirName(searchTrimmed);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function sanitizeDirName(raw) {
|
|
161
|
+
const s = raw.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/_+/g, "_").replace(/^\.+|\.+$/g, "").replace(/^_|_$/g, "");
|
|
162
|
+
return s || "export";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/export/writeArtifacts.ts
|
|
166
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
167
|
+
import { join as join2 } from "path";
|
|
168
|
+
|
|
169
|
+
// src/export/render.ts
|
|
170
|
+
function escCell(s) {
|
|
171
|
+
if (s === null || s === void 0) return "";
|
|
172
|
+
return String(s).replace(/\|/g, "\\|").replace(/\r\n/g, "\n").replace(/\n/g, "<br>");
|
|
173
|
+
}
|
|
174
|
+
function pathnameFromBugUrl(urlStr) {
|
|
175
|
+
if (!urlStr.trim()) return "";
|
|
176
|
+
try {
|
|
177
|
+
return new URL(urlStr).pathname || "";
|
|
178
|
+
} catch {
|
|
179
|
+
return "";
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function pickAttr(attrs, targetAttrs, key) {
|
|
183
|
+
const a = attrs?.[key];
|
|
184
|
+
if (typeof a === "string" && a.trim()) return a;
|
|
185
|
+
const t = targetAttrs?.[key];
|
|
186
|
+
return typeof t === "string" ? t : "";
|
|
187
|
+
}
|
|
188
|
+
function navigationSummary(meta) {
|
|
189
|
+
if (!meta) return "";
|
|
190
|
+
const path = typeof meta.path === "string" ? meta.path : "";
|
|
191
|
+
const url = typeof meta.url === "string" ? meta.url : "";
|
|
192
|
+
const title = typeof meta.title === "string" ? meta.title : "";
|
|
193
|
+
const parts = [path && `path=${path}`, url && url !== path && `url=${url}`, title && `title=${title}`].filter(
|
|
194
|
+
Boolean
|
|
195
|
+
);
|
|
196
|
+
return parts.join(" \xB7 ");
|
|
197
|
+
}
|
|
198
|
+
function actionsTable(actions) {
|
|
199
|
+
if (!Array.isArray(actions) || actions.length === 0) {
|
|
200
|
+
return "_\uFF08\u65E0 actions \u6216\u5C1A\u672A\u91C7\u96C6\uFF09_\n";
|
|
201
|
+
}
|
|
202
|
+
const lines = [
|
|
203
|
+
"| # | type | target | timestamp | navigation\uFF08path / url / title\uFF09 | text\uFF08innerText\uFF09 | targetText | id | aria-label | data-openreplay-label | class |",
|
|
204
|
+
"|---|------|--------|-----------|--------------------------------------|-------------------|------------|----|-----------|------------------------|-------|"
|
|
205
|
+
];
|
|
206
|
+
actions.forEach((a, idx) => {
|
|
207
|
+
if (!a || typeof a !== "object") return;
|
|
208
|
+
const o = a;
|
|
209
|
+
const type = escCell(o.type);
|
|
210
|
+
const target = escCell(o.target);
|
|
211
|
+
const ts = escCell(o.timestamp);
|
|
212
|
+
const meta = o.metadata && typeof o.metadata === "object" ? o.metadata : null;
|
|
213
|
+
const text = escCell(meta?.text);
|
|
214
|
+
const targetText = escCell(meta?.targetText);
|
|
215
|
+
const attrs = meta?.attributes && typeof meta.attributes === "object" ? meta.attributes : null;
|
|
216
|
+
const targetAttrs = meta?.targetAttributes && typeof meta.targetAttributes === "object" ? meta.targetAttributes : null;
|
|
217
|
+
const nav = o.type === "navigation" && meta ? escCell(navigationSummary(meta)) : "";
|
|
218
|
+
const id = escCell(pickAttr(attrs, targetAttrs, "id"));
|
|
219
|
+
const aria = escCell(pickAttr(attrs, targetAttrs, "aria-label"));
|
|
220
|
+
const label = escCell(pickAttr(attrs, targetAttrs, "data-openreplay-label"));
|
|
221
|
+
const cls = escCell(pickAttr(attrs, targetAttrs, "class"));
|
|
222
|
+
lines.push(
|
|
223
|
+
`| ${idx + 1} | ${type} | ${target} | ${ts} | ${nav} | ${text} | ${targetText} | ${id} | ${aria} | ${label} | ${cls} |`
|
|
224
|
+
);
|
|
225
|
+
});
|
|
226
|
+
return lines.join("\n") + "\n";
|
|
227
|
+
}
|
|
228
|
+
function pickedDomSection(id, rows, reportRel) {
|
|
229
|
+
if (rows.length === 0) return "";
|
|
230
|
+
const tableLines = [
|
|
231
|
+
"| sortOrder | attachment id | \u672C\u5730 HTML | contentType | sizeBytes |",
|
|
232
|
+
"|-----------|---------------|-----------|-------------|-----------|"
|
|
233
|
+
];
|
|
234
|
+
for (const row of rows) {
|
|
235
|
+
const htmlCell = row.localRel ? `[\`${row.localRel.replace(/^\.\//, "")}\`](${row.localRel})` : escCell(row.downloadError ? `\u4E0B\u8F7D\u5931\u8D25\uFF1A${row.downloadError}` : "\u4E0B\u8F7D\u5931\u8D25");
|
|
236
|
+
tableLines.push(
|
|
237
|
+
`| ${row.sortOrder} | \`${escCell(row.id)}\` | ${htmlCell} | ${escCell(row.contentType)} | ${row.sizeBytes} |`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
return `## \u7528\u6237\u6807\u6CE8 DOM\uFF08attachments / picked_dom\uFF09
|
|
241
|
+
|
|
242
|
+
\u4EE5\u4E0B HTML \u4E3A**\u7528\u6237\u5728\u63D0\u4EA4\u7F3A\u9677\u65F6\u6846\u9009/\u6807\u6CE8\u7684\u9875\u9762\u8282\u70B9 DOM \u7247\u6BB5**\uFF0C\u7528\u4E8E\u5728\u4E1A\u52A1\u4EE3\u7801\u5E93\u4E2D\u5BF9\u7167 DOM \u7ED3\u6784\u3001class \u4E0E\u53EF\u89C1\u6587\u6848\u3002
|
|
243
|
+
|
|
244
|
+
${tableLines.join("\n")}
|
|
245
|
+
|
|
246
|
+
> \u5B8C\u6574 \`attachments\` \u5143\u6570\u636E\u89C1 [\`${reportRel.replace(/^\.\//, "")}\`](${reportRel})\u3002
|
|
247
|
+
|
|
248
|
+
`;
|
|
249
|
+
}
|
|
250
|
+
function renderBugMarkdown(id, report, rel, actionsForTable, pickedDomRows = []) {
|
|
251
|
+
const r = report && typeof report === "object" ? report : {};
|
|
252
|
+
const url = typeof r.url === "string" ? r.url : "";
|
|
253
|
+
const description = typeof r.description === "string" ? r.description : "";
|
|
254
|
+
const status = typeof r.status === "string" ? r.status : "";
|
|
255
|
+
const submissionStatus = typeof r.submissionStatus === "string" ? r.submissionStatus : "";
|
|
256
|
+
const debuggerIngestionStatus = typeof r.debuggerIngestionStatus === "string" ? r.debuggerIngestionStatus : "";
|
|
257
|
+
const pathFromUrl = pathnameFromBugUrl(url);
|
|
258
|
+
const attachmentUrl = typeof r.attachmentUrl === "string" ? r.attachmentUrl : "";
|
|
259
|
+
const attachmentType = typeof r.attachmentType === "string" ? r.attachmentType : "";
|
|
260
|
+
const networkLine = rel.networkRel ? `- **networkRequests**\uFF1A[\`${rel.networkRel.replace(/^\.\//, "")}\`](${rel.networkRel})\uFF08\u539F\u59CB\u8BF7\u6C42/\u54CD\u5E94\u4F53\uFF0C\u4F53\u79EF\u53EF\u80FD\u8F83\u5927\uFF09
|
|
261
|
+
` : "";
|
|
262
|
+
const pickedDomBlock = pickedDomSection(id, pickedDomRows, rel.reportRel);
|
|
263
|
+
return `# Bug report \`${id}\`
|
|
264
|
+
|
|
265
|
+
## Agent \u6307\u5F15\uFF08\u4EE3\u7801\u5E93\u68C0\u7D22\uFF09
|
|
266
|
+
|
|
267
|
+
- **\u52FF\u5C06\u6587\u6863/\u7A97\u53E3\u6807\u9898**\uFF08\u5982 \`getById.title\`\u3001\`metadata.pageTitle\`\uFF09\u4F5C\u4E3A\u552F\u4E00\u68C0\u7D22\u5173\u952E\u8BCD\uFF1B\u5B83\u4EEC\u4F1A\u968F\u8DEF\u7531\u4E0E\u4EA7\u54C1\u6587\u6848\u53D8\u5316\u3002
|
|
268
|
+
- **\u4F18\u5148\u5728\u4EE3\u7801\u5E93\u4E2D\u641C\u7D22**\uFF1A\`url\`\u3001\u8DEF\u7531 path\u3001**\u95EE\u9898\u63CF\u8FF0\uFF08description\uFF09**\u3001\u4E0B\u65B9 **\u7528\u6237\u6807\u6CE8 DOM\uFF08picked_dom\uFF09** HTML\u3001\`data-openreplay-label\`\u3001\u7A33\u5B9A \`id\`\u3001\`aria-label\`\u3001\`class\`\uFF08\u542B Tailwind \u539F\u5B50\u7C7B\uFF09\u3001\`targetText\`\uFF08\u5E38\u6BD4 innerText \u66F4\u5E72\u51C0\uFF09\u3001\u4EE5\u53CA **\u590D\u73B0\u6B65\u9AA4\u8868** \u4E2D\u7684\u53EF\u89C1\u6587\u6848\u7247\u6BB5\u3002
|
|
269
|
+
|
|
270
|
+
## \u73AF\u5883\u4E0E\u8DEF\u7531
|
|
271
|
+
|
|
272
|
+
| \u5B57\u6BB5 | \u503C |
|
|
273
|
+
|------|-----|
|
|
274
|
+
| id | \`${escCell(id)}\` |
|
|
275
|
+
| url | \`${escCell(url)}\` |
|
|
276
|
+
| path\uFF08\u4ECE url \u89E3\u6790\uFF0C\u4FBF\u4E8E\u641C\u8DEF\u7531\uFF09 | \`${escCell(pathFromUrl)}\` |
|
|
277
|
+
| status | ${escCell(status)} |
|
|
278
|
+
| submissionStatus | ${escCell(submissionStatus)} |
|
|
279
|
+
| debuggerIngestionStatus | ${escCell(debuggerIngestionStatus)} |
|
|
280
|
+
| attachmentUrl | ${escCell(attachmentUrl)} |
|
|
281
|
+
| attachmentType | ${escCell(attachmentType)} |
|
|
282
|
+
|
|
283
|
+
## \u95EE\u9898\u63CF\u8FF0\uFF08description\uFF09
|
|
284
|
+
|
|
285
|
+
${description ? description : "_\uFF08\u7A7A\uFF09_"}
|
|
286
|
+
|
|
287
|
+
## \u590D\u73B0\u6B65\u9AA4\u7D22\u5F15\uFF08\u6765\u81EA debugger actions\uFF09
|
|
288
|
+
|
|
289
|
+
${actionsTable(Array.isArray(actionsForTable) ? actionsForTable : [])}
|
|
290
|
+
|
|
291
|
+
${pickedDomBlock}> \u5B8C\u6574 \`actions\` / \`logs\` \u7B49\u89C1 sidecar\uFF1A[\`${rel.debuggerRel.replace(/^\.\//, "")}\`](${rel.debuggerRel})
|
|
292
|
+
|
|
293
|
+
## \u539F\u59CB\u62A5\u544A JSON\uFF08getById\uFF09
|
|
294
|
+
|
|
295
|
+
\u5B8C\u6574\u5B57\u6BB5\uFF08\u4E0E RPC \u5BF9\u9F50\uFF09\u89C1\uFF1A[\`${rel.reportRel.replace(/^\.\//, "")}\`](${rel.reportRel})
|
|
296
|
+
|
|
297
|
+
${networkLine}
|
|
298
|
+
`;
|
|
299
|
+
}
|
|
300
|
+
function extractNetworkRequests(debuggerPayload) {
|
|
301
|
+
if (!debuggerPayload || typeof debuggerPayload !== "object") return null;
|
|
302
|
+
const d = debuggerPayload;
|
|
303
|
+
if ("networkRequests" in d) return d.networkRequests;
|
|
304
|
+
const json = d.json;
|
|
305
|
+
if (json && typeof json === "object" && "networkRequests" in json) {
|
|
306
|
+
return json.networkRequests;
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
function extractActions(debuggerPayload) {
|
|
311
|
+
if (!debuggerPayload || typeof debuggerPayload !== "object") return [];
|
|
312
|
+
const d = debuggerPayload;
|
|
313
|
+
if (Array.isArray(d.actions)) return d.actions;
|
|
314
|
+
const json = d.json;
|
|
315
|
+
if (json && typeof json === "object") {
|
|
316
|
+
const j = json;
|
|
317
|
+
if (Array.isArray(j.actions)) return j.actions;
|
|
318
|
+
}
|
|
319
|
+
return [];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// src/export/writeArtifacts.ts
|
|
323
|
+
function shouldWriteNetworkSidecar(net) {
|
|
324
|
+
if (net === null || net === void 0) return false;
|
|
325
|
+
if (Array.isArray(net)) return net.length > 0;
|
|
326
|
+
if (typeof net === "object") return Object.keys(net).length > 0;
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
function writeBugArtifacts(outDir, id, report, debuggerPayload, pickedDomRows = []) {
|
|
330
|
+
mkdirSync2(outDir, { recursive: true });
|
|
331
|
+
const reportPath = join2(outDir, `${id}.report.json`);
|
|
332
|
+
const debuggerPath = join2(outDir, `${id}.debugger.json`);
|
|
333
|
+
writeFileSync2(reportPath, JSON.stringify(report, null, 2), "utf8");
|
|
334
|
+
writeFileSync2(debuggerPath, JSON.stringify(debuggerPayload, null, 2), "utf8");
|
|
335
|
+
const net = extractNetworkRequests(debuggerPayload);
|
|
336
|
+
let networkRel = null;
|
|
337
|
+
let networkPath;
|
|
338
|
+
if (shouldWriteNetworkSidecar(net)) {
|
|
339
|
+
networkPath = join2(outDir, `${id}.network.json`);
|
|
340
|
+
writeFileSync2(networkPath, JSON.stringify(net, null, 2), "utf8");
|
|
341
|
+
networkRel = `./${id}.network.json`;
|
|
342
|
+
}
|
|
343
|
+
const actions = extractActions(debuggerPayload);
|
|
344
|
+
const md = renderBugMarkdown(
|
|
345
|
+
id,
|
|
346
|
+
report,
|
|
347
|
+
{
|
|
348
|
+
reportRel: `./${id}.report.json`,
|
|
349
|
+
debuggerRel: `./${id}.debugger.json`,
|
|
350
|
+
networkRel
|
|
351
|
+
},
|
|
352
|
+
actions,
|
|
353
|
+
pickedDomRows
|
|
354
|
+
);
|
|
355
|
+
const mdPath = join2(outDir, `${id}.md`);
|
|
356
|
+
writeFileSync2(mdPath, md, "utf8");
|
|
357
|
+
return {
|
|
358
|
+
md: mdPath,
|
|
359
|
+
reportJson: reportPath,
|
|
360
|
+
debuggerJson: debuggerPath,
|
|
361
|
+
networkJson: networkPath
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// src/client.ts
|
|
366
|
+
var CrikketClient = class {
|
|
367
|
+
baseUrl;
|
|
368
|
+
organizationId;
|
|
369
|
+
fetchImpl;
|
|
370
|
+
constructor(opts) {
|
|
371
|
+
const baseUrl = opts.baseUrl?.trim();
|
|
372
|
+
if (!baseUrl) {
|
|
373
|
+
throw new Error("baseUrl \u4E0D\u80FD\u4E3A\u7A7A");
|
|
374
|
+
}
|
|
375
|
+
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
376
|
+
this.organizationId = opts.organizationId?.trim() || void 0;
|
|
377
|
+
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
378
|
+
}
|
|
379
|
+
resolveOrganizationId(input) {
|
|
380
|
+
const org = input?.trim() || this.organizationId;
|
|
381
|
+
if (!org) {
|
|
382
|
+
throw new Error("organizationId \u672A\u63D0\u4F9B\uFF08\u6784\u9020\u53C2\u6570\u6216\u65B9\u6CD5\u53C2\u6570\uFF09");
|
|
383
|
+
}
|
|
384
|
+
return org;
|
|
385
|
+
}
|
|
386
|
+
async rpc(procedurePath, jsonInput) {
|
|
387
|
+
return rpcPost(this.baseUrl, procedurePath, jsonInput, this.fetchImpl);
|
|
388
|
+
}
|
|
389
|
+
async listByOrganizationId(input) {
|
|
390
|
+
const organizationId = this.resolveOrganizationId(input.organizationId);
|
|
391
|
+
const raw = await this.rpc("bugReport/listByOrganizationId", {
|
|
392
|
+
organizationId,
|
|
393
|
+
page: input.page ?? 1,
|
|
394
|
+
perPage: input.perPage ?? 12,
|
|
395
|
+
search: input.search,
|
|
396
|
+
sort: input.sort ?? "newest",
|
|
397
|
+
statuses: input.statuses,
|
|
398
|
+
priorities: input.priorities
|
|
399
|
+
});
|
|
400
|
+
if (!raw || typeof raw !== "object") {
|
|
401
|
+
throw new Error(`list \u8FD4\u56DE\u975E\u5BF9\u8C61: ${JSON.stringify(raw).slice(0, 500)}`);
|
|
402
|
+
}
|
|
403
|
+
return raw;
|
|
404
|
+
}
|
|
405
|
+
async getDashboardStats(input = {}) {
|
|
406
|
+
const organizationId = this.resolveOrganizationId(input.organizationId);
|
|
407
|
+
return this.rpc("bugReport/getDashboardStatsByOrganizationId", {
|
|
408
|
+
organizationId,
|
|
409
|
+
search: input.search,
|
|
410
|
+
statuses: input.statuses,
|
|
411
|
+
priorities: input.priorities
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
async getBugById(id) {
|
|
415
|
+
const trimmed = id.trim();
|
|
416
|
+
if (!trimmed) throw new Error("id \u4E0D\u80FD\u4E3A\u7A7A");
|
|
417
|
+
return this.rpc("bugReport/getByIdUnauthenticated", { id: trimmed });
|
|
418
|
+
}
|
|
419
|
+
async getDebuggerEvents(id) {
|
|
420
|
+
const trimmed = id.trim();
|
|
421
|
+
if (!trimmed) throw new Error("id \u4E0D\u80FD\u4E3A\u7A7A");
|
|
422
|
+
return this.rpc("bugReport/getDebuggerEventsUnauthenticated", { id: trimmed });
|
|
423
|
+
}
|
|
424
|
+
async updateBulkStatus(input) {
|
|
425
|
+
return updateBulkStatus({
|
|
426
|
+
baseUrl: this.baseUrl,
|
|
427
|
+
organizationId: this.resolveOrganizationId(input.organizationId),
|
|
428
|
+
bugIds: input.ids,
|
|
429
|
+
status: input.status,
|
|
430
|
+
fetchImpl: this.fetchImpl
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
async markInProgress(input) {
|
|
434
|
+
return this.updateBulkStatus({ ...input, status: "in_progress" });
|
|
435
|
+
}
|
|
436
|
+
async markResolved(input) {
|
|
437
|
+
return this.updateBulkStatus({ ...input, status: "resolved" });
|
|
438
|
+
}
|
|
439
|
+
async markOpen(input) {
|
|
440
|
+
return this.updateBulkStatus({ ...input, status: "open" });
|
|
441
|
+
}
|
|
442
|
+
async downloadBugs(input) {
|
|
443
|
+
const outDir = input.outDir;
|
|
444
|
+
mkdirSync3(outDir, { recursive: true });
|
|
445
|
+
const results = [];
|
|
446
|
+
for (const rawId of input.ids) {
|
|
447
|
+
const id = rawId.trim();
|
|
448
|
+
if (!id) continue;
|
|
449
|
+
results.push(await this.downloadOneBug(outDir, id));
|
|
450
|
+
}
|
|
451
|
+
return results;
|
|
452
|
+
}
|
|
453
|
+
async exportOpenBugs(input) {
|
|
454
|
+
const search = input.search.trim();
|
|
455
|
+
if (!search) {
|
|
456
|
+
throw new Error("search \u4E0D\u80FD\u4E3A\u7A7A\uFF08trim \u540E\uFF09");
|
|
457
|
+
}
|
|
458
|
+
const organizationId = this.resolveOrganizationId(input.organizationId);
|
|
459
|
+
const targetDir = join3(input.outDir, searchNormalized(search));
|
|
460
|
+
mkdirSync3(targetDir, { recursive: true });
|
|
461
|
+
const ids = await this.listAllOpenIds(organizationId, search);
|
|
462
|
+
const downloadResults = [];
|
|
463
|
+
for (const id of ids) {
|
|
464
|
+
downloadResults.push(await this.downloadOneBug(targetDir, id));
|
|
465
|
+
}
|
|
466
|
+
return { ids, outDir: targetDir, downloadResults };
|
|
467
|
+
}
|
|
468
|
+
async listAllOpenIds(organizationId, searchTrimmed) {
|
|
469
|
+
const ids = [];
|
|
470
|
+
const seen = /* @__PURE__ */ new Set();
|
|
471
|
+
let page = 1;
|
|
472
|
+
const perPage = 50;
|
|
473
|
+
while (true) {
|
|
474
|
+
const data = await this.listByOrganizationId({
|
|
475
|
+
organizationId,
|
|
476
|
+
page,
|
|
477
|
+
perPage,
|
|
478
|
+
search: searchTrimmed,
|
|
479
|
+
sort: "newest",
|
|
480
|
+
statuses: ["open"]
|
|
481
|
+
});
|
|
482
|
+
const items = Array.isArray(data.items) ? data.items : [];
|
|
483
|
+
for (const it of items) {
|
|
484
|
+
if (it && typeof it === "object" && typeof it.id === "string") {
|
|
485
|
+
const id = it.id;
|
|
486
|
+
if (!seen.has(id)) {
|
|
487
|
+
seen.add(id);
|
|
488
|
+
ids.push(id);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
const pagination = data.pagination && typeof data.pagination === "object" ? data.pagination : null;
|
|
493
|
+
const hasNext = pagination && pagination.hasNextPage === true;
|
|
494
|
+
if (!hasNext) break;
|
|
495
|
+
page += 1;
|
|
496
|
+
}
|
|
497
|
+
return ids;
|
|
498
|
+
}
|
|
499
|
+
async downloadOneBug(outDir, id) {
|
|
500
|
+
try {
|
|
501
|
+
const report = await this.getBugById(id);
|
|
502
|
+
const dbg = await this.getDebuggerEvents(id);
|
|
503
|
+
const pickedDomRows = await downloadPickedDomAttachments(
|
|
504
|
+
report,
|
|
505
|
+
outDir,
|
|
506
|
+
id,
|
|
507
|
+
this.fetchImpl
|
|
508
|
+
);
|
|
509
|
+
const paths = writeBugArtifacts(outDir, id, report, dbg, pickedDomRows);
|
|
510
|
+
return { id, ok: true, paths };
|
|
511
|
+
} catch (e) {
|
|
512
|
+
const message = e instanceof RpcError ? e.message : e instanceof Error ? e.message : String(e);
|
|
513
|
+
return { id, ok: false, error: message };
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
export {
|
|
518
|
+
CrikketClient,
|
|
519
|
+
RpcError,
|
|
520
|
+
assertBulkUpdatedCount,
|
|
521
|
+
rpcPost,
|
|
522
|
+
searchNormalized,
|
|
523
|
+
uniqueBugIds,
|
|
524
|
+
unwrapProcedureResult,
|
|
525
|
+
updateBulkStatus,
|
|
526
|
+
validateBugIdList
|
|
527
|
+
};
|
package/dist/rpc.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** 解析 tRPC / oRPC HTTP 常见响应形态,取出 procedure 的 data.json。 */
|
|
2
|
+
declare function unwrapProcedureResult(body: unknown): unknown;
|
|
3
|
+
declare class RpcError extends Error {
|
|
4
|
+
readonly status: number;
|
|
5
|
+
readonly statusText: string;
|
|
6
|
+
readonly bodyPreview: string;
|
|
7
|
+
constructor(status: number, statusText: string, bodyPreview: string);
|
|
8
|
+
}
|
|
9
|
+
declare function rpcPost(rpcBaseUrl: string, procedurePath: string, jsonInput: Record<string, unknown>, fetchImpl?: typeof fetch): Promise<unknown>;
|
|
10
|
+
|
|
11
|
+
export { RpcError, rpcPost, unwrapProcedureResult };
|
package/dist/rpc.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@d2c2d/crikket-sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Crikket anonymous oRPC client: export bugs, download artifacts, bulk status updates.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=18"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./rpc": {
|
|
20
|
+
"types": "./dist/rpc.d.ts",
|
|
21
|
+
"default": "./dist/rpc.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"test": "tsx --test tests/rpc.test.ts tests/bulk.test.ts tests/client.test.ts tests/export/normalizeSearch.test.ts tests/export/attachments.test.ts tests/export/render.test.ts"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"crikket",
|
|
31
|
+
"bug-report",
|
|
32
|
+
"orpc"
|
|
33
|
+
],
|
|
34
|
+
"author": "",
|
|
35
|
+
"license": "ISC",
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^25.6.2",
|
|
38
|
+
"tsup": "^8.5.1",
|
|
39
|
+
"tsx": "^4.19.2",
|
|
40
|
+
"typescript": "^6.0.3"
|
|
41
|
+
}
|
|
42
|
+
}
|