@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 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
+ };
@@ -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
@@ -0,0 +1,10 @@
1
+ import {
2
+ RpcError,
3
+ rpcPost,
4
+ unwrapProcedureResult
5
+ } from "./chunk-VBFPP5MD.js";
6
+ export {
7
+ RpcError,
8
+ rpcPost,
9
+ unwrapProcedureResult
10
+ };
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
+ }