@skillbuilder/minitool-core 0.0.0-beta-20261027053041051

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.
Files changed (61) hide show
  1. package/README.md +74 -0
  2. package/lib/di/index.js +21 -0
  3. package/lib/di/modules.js +23 -0
  4. package/lib/di/tokens.js +12 -0
  5. package/lib/index.js +29 -0
  6. package/lib/runtime.js +42 -0
  7. package/lib/services/index.js +19 -0
  8. package/lib/services/minitool-api.js +125 -0
  9. package/lib/services/minitool-contract.js +76 -0
  10. package/lib/services/minitool-service.js +161 -0
  11. package/lib/services/upload-service.js +183 -0
  12. package/lib/utils/config-merger.js +63 -0
  13. package/lib/utils/cos-uploader.js +120 -0
  14. package/lib/utils/env-helper.js +4 -0
  15. package/lib/utils/helper.js +46 -0
  16. package/lib/utils/loading.js +52 -0
  17. package/lib/utils/logger.js +83 -0
  18. package/lib/utils/open-browser.js +32 -0
  19. package/lib/utils/openChrome.applescript +78 -0
  20. package/lib/utils/output.js +19 -0
  21. package/lib/utils/port-finder.js +43 -0
  22. package/lib/utils/project-validator.js +233 -0
  23. package/lib/utils/request.js +73 -0
  24. package/lib/utils/table.js +29 -0
  25. package/lib/utils/to-hump.js +5 -0
  26. package/lib/utils/tracker/ai-apm.js +119 -0
  27. package/lib/utils/tracker/apm.js +73 -0
  28. package/lib/utils/tracker/cli-apm.js +188 -0
  29. package/lib/utils/tracker/device.js +30 -0
  30. package/lib/utils/tracker/index.js +21 -0
  31. package/lib/utils/tracker/tracker.js +2 -0
  32. package/package.json +32 -0
  33. package/types/di/index.d.ts +3 -0
  34. package/types/di/modules.d.ts +9 -0
  35. package/types/di/tokens.d.ts +9 -0
  36. package/types/index.d.ts +14 -0
  37. package/types/runtime.d.ts +24 -0
  38. package/types/services/index.d.ts +3 -0
  39. package/types/services/minitool-api.d.ts +30 -0
  40. package/types/services/minitool-contract.d.ts +103 -0
  41. package/types/services/minitool-service.d.ts +29 -0
  42. package/types/services/upload-service.d.ts +48 -0
  43. package/types/utils/config-merger.d.ts +6 -0
  44. package/types/utils/cos-uploader.d.ts +45 -0
  45. package/types/utils/env-helper.d.ts +1 -0
  46. package/types/utils/helper.d.ts +8 -0
  47. package/types/utils/loading.d.ts +15 -0
  48. package/types/utils/logger.d.ts +27 -0
  49. package/types/utils/open-browser.d.ts +2 -0
  50. package/types/utils/output.d.ts +7 -0
  51. package/types/utils/port-finder.d.ts +2 -0
  52. package/types/utils/project-validator.d.ts +25 -0
  53. package/types/utils/request.d.ts +26 -0
  54. package/types/utils/table.d.ts +9 -0
  55. package/types/utils/to-hump.d.ts +1 -0
  56. package/types/utils/tracker/ai-apm.d.ts +68 -0
  57. package/types/utils/tracker/apm.d.ts +21 -0
  58. package/types/utils/tracker/cli-apm.d.ts +47 -0
  59. package/types/utils/tracker/device.d.ts +8 -0
  60. package/types/utils/tracker/index.d.ts +5 -0
  61. package/types/utils/tracker/tracker.d.ts +22 -0
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.formatHelpInfo = formatHelpInfo;
7
+ exports.outputHelpInfo = outputHelpInfo;
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ /** 通用 CLI 帮助格式化器;不再写死旧 rcb agent/fun/env 命令。 */
10
+ function formatHelpInfo(title, commands) {
11
+ const lines = [chalk_1.default.cyan.bold(title), ""];
12
+ for (const command of commands) {
13
+ lines.push(` ${chalk_1.default.cyan(command.name.padEnd(22))} ${command.description}`);
14
+ }
15
+ return lines.join("\n");
16
+ }
17
+ function outputHelpInfo(title, commands) {
18
+ console.log(formatHelpInfo(title, commands));
19
+ }
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.findAvailablePort = findAvailablePort;
7
+ exports.isPortAvailable = isPortAvailable;
8
+ const net_1 = __importDefault(require("net"));
9
+ async function findAvailablePort(startPort) {
10
+ return new Promise((resolve, reject) => {
11
+ const server = net_1.default.createServer();
12
+ server.on("error", (err) => {
13
+ if (err.code === "EADDRINUSE") {
14
+ // 端口被占用,尝试下一个端口
15
+ server.close();
16
+ resolve(findAvailablePort(startPort + 1));
17
+ }
18
+ else {
19
+ reject(err);
20
+ }
21
+ });
22
+ server.listen(startPort, () => {
23
+ const { port } = server.address();
24
+ server.close(() => resolve(port));
25
+ });
26
+ });
27
+ }
28
+ async function isPortAvailable(port) {
29
+ return new Promise((resolve) => {
30
+ const server = net_1.default.createServer();
31
+ server.on("error", (err) => {
32
+ if (err.code === "EADDRINUSE") {
33
+ resolve(false);
34
+ }
35
+ else {
36
+ resolve(false);
37
+ }
38
+ });
39
+ server.listen(port, () => {
40
+ server.close(() => resolve(true));
41
+ });
42
+ });
43
+ }
@@ -0,0 +1,233 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.ProjectValidator = void 0;
40
+ exports.validateMiniToolArtifact = validateMiniToolArtifact;
41
+ exports.inspectMiniToolFile = inspectMiniToolFile;
42
+ const node_fs_1 = __importDefault(require("node:fs"));
43
+ const node_path_1 = __importDefault(require("node:path"));
44
+ /** 小工具产物校验;旧 Agent 云函数项目结构与上传预检不再适用。 */
45
+ function validateMiniToolArtifact(inputPath) {
46
+ const filePath = node_path_1.default.resolve(inputPath);
47
+ const stat = node_fs_1.default.lstatSync(filePath);
48
+ if (!stat.isFile() || stat.size === 0 || node_path_1.default.extname(filePath).toLowerCase() !== ".zip") {
49
+ throw new Error("上传文件必须是非空的 .zip 文件");
50
+ }
51
+ const signature = Buffer.alloc(4);
52
+ const descriptor = node_fs_1.default.openSync(filePath, "r");
53
+ try {
54
+ node_fs_1.default.readSync(descriptor, signature, 0, 4, 0);
55
+ }
56
+ finally {
57
+ node_fs_1.default.closeSync(descriptor);
58
+ }
59
+ const zipMagic = `${signature[2]}:${signature[3]}`;
60
+ if (signature[0] !== 0x50 || signature[1] !== 0x4b || !["3:4", "5:6", "7:8"].includes(zipMagic)) {
61
+ throw new Error("文件不是有效的 ZIP 产物");
62
+ }
63
+ return { filePath, size: stat.size };
64
+ }
65
+ /** 与旧校验器保持相同的 validate() 结果形态,校验目标改为小工具 ZIP。 */
66
+ class ProjectValidator {
67
+ artifactPath;
68
+ constructor(artifactPath) {
69
+ this.artifactPath = artifactPath;
70
+ }
71
+ async validate() {
72
+ try {
73
+ await inspectMiniToolFile(this.artifactPath, "pkg");
74
+ return { valid: true, errors: [] };
75
+ }
76
+ catch (error) {
77
+ return {
78
+ valid: false,
79
+ errors: [error instanceof Error ? error.message : "小工具产物校验失败"],
80
+ };
81
+ }
82
+ }
83
+ }
84
+ exports.ProjectValidator = ProjectValidator;
85
+ /** 先限制大小,再校验压缩目录与 CRC;返回与校验内容一致的上传快照。 */
86
+ async function inspectMiniToolFile(inputPath, type) {
87
+ const { createHash } = await Promise.resolve().then(() => __importStar(require("node:crypto")));
88
+ const filePath = node_path_1.default.resolve(inputPath);
89
+ const descriptor = node_fs_1.default.openSync(filePath, node_fs_1.default.constants.O_RDONLY | node_fs_1.default.constants.O_NOFOLLOW);
90
+ let bytes;
91
+ try {
92
+ const stat = node_fs_1.default.fstatSync(descriptor);
93
+ if (!stat.isFile() || stat.size === 0 || stat.size > 10 * 1024 * 1024)
94
+ throw new Error("上传文件须为非空普通文件,且不超过 10 MiB");
95
+ // 有界读取,避免校验后文件并发增大导致无限分配。
96
+ const buffer = Buffer.alloc(10 * 1024 * 1024 + 1);
97
+ let offset = 0;
98
+ while (offset < buffer.length) {
99
+ const read = node_fs_1.default.readSync(descriptor, buffer, offset, buffer.length - offset, null);
100
+ if (!read)
101
+ break;
102
+ offset += read;
103
+ }
104
+ if (!offset || offset > 10 * 1024 * 1024)
105
+ throw new Error("上传文件须非空且不超过 10 MiB");
106
+ bytes = buffer.subarray(0, offset);
107
+ }
108
+ finally {
109
+ node_fs_1.default.closeSync(descriptor);
110
+ }
111
+ let format;
112
+ if (type === "pkg") {
113
+ format = "zip";
114
+ const { default: JSZip } = await Promise.resolve().then(() => __importStar(require("jszip")));
115
+ const count = validateZipDirectory(bytes);
116
+ const zip = await JSZip.loadAsync(bytes, { createFolders: false });
117
+ const entries = Object.values(zip.files);
118
+ if (entries.length !== count)
119
+ throw new Error("ZIP 包含重复路径");
120
+ if (entries.length > 2000)
121
+ throw new Error("ZIP 文件数量超过 2000");
122
+ let total = 0;
123
+ for (const entry of entries) {
124
+ const original = entry.unsafeOriginalName ?? entry.name;
125
+ const parts = original.replace(/\/$/, "").split("/");
126
+ if (!original ||
127
+ original.includes("\\") ||
128
+ original.includes("\0") ||
129
+ original.startsWith("/") ||
130
+ /^[A-Za-z]:/.test(original) ||
131
+ parts.some((part) => [".", "..", ""].includes(part)) ||
132
+ (Number(entry.unixPermissions) & 0xf000) === 0xa000)
133
+ throw new Error("ZIP 包含不安全路径或符号链接");
134
+ // JSZip 在读取条目之前保留中央目录声明的解压大小。
135
+ const size = entry._data
136
+ ?.uncompressedSize ?? 0;
137
+ total += size;
138
+ if (!Number.isSafeInteger(total) || total > 30 * 1024 * 1024)
139
+ throw new Error("ZIP 解压后不能超过 30 MiB");
140
+ }
141
+ if (!zip.files["index.html"] || zip.files["index.html"].dir)
142
+ throw new Error("ZIP 根目录必须包含 index.html");
143
+ let unpacked = 0;
144
+ for (const entry of entries) {
145
+ if (entry.dir)
146
+ continue;
147
+ let crc = 0xffffffff;
148
+ const stream = entry.nodeStream("nodebuffer");
149
+ await new Promise((resolve, reject) => {
150
+ stream.on("data", (buffer) => {
151
+ unpacked += buffer.length;
152
+ if (unpacked > 30 * 1024 * 1024) {
153
+ stream.destroy(new Error("ZIP 解压后不能超过 30 MiB"));
154
+ return;
155
+ }
156
+ for (const byte of buffer)
157
+ crc = (crc >>> 8) ^ CRC32_TABLE[(crc ^ byte) & 0xff];
158
+ });
159
+ stream.once("end", resolve);
160
+ stream.once("error", reject);
161
+ });
162
+ const expected = entry._data?.crc32;
163
+ if (expected === undefined || (crc ^ 0xffffffff) >>> 0 !== expected >>> 0)
164
+ throw new Error("ZIP 文件 CRC 校验失败");
165
+ }
166
+ }
167
+ else {
168
+ if (bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])))
169
+ format = "png";
170
+ else if (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff)
171
+ format = "jpg";
172
+ else if (["GIF87a", "GIF89a"].includes(bytes.toString("ascii", 0, 6)))
173
+ format = "gif";
174
+ else if (bytes.toString("ascii", 0, 4) === "RIFF" && bytes.toString("ascii", 8, 12) === "WEBP")
175
+ format = "webp";
176
+ else
177
+ throw new Error("图片仅支持 PNG、JPEG、GIF、WebP");
178
+ }
179
+ return {
180
+ filePath,
181
+ type,
182
+ format,
183
+ size: bytes.length,
184
+ bytes,
185
+ md5: createHash("md5").update(bytes).digest("hex"),
186
+ sha256: createHash("sha256").update(bytes).digest("hex"),
187
+ };
188
+ }
189
+ /** 在 JSZip 合并同名条目前校验中央目录,拒绝重复项与超大解压声明。 */
190
+ function validateZipDirectory(bytes) {
191
+ let end = -1;
192
+ for (let offset = bytes.length - 22; offset >= Math.max(0, bytes.length - 65557); offset--) {
193
+ if (bytes.readUInt32LE(offset) === 0x06054b50 &&
194
+ offset + 22 + bytes.readUInt16LE(offset + 20) === bytes.length) {
195
+ end = offset;
196
+ break;
197
+ }
198
+ }
199
+ if (end < 0 || bytes.readUInt16LE(end + 4) || bytes.readUInt16LE(end + 6))
200
+ throw new Error("ZIP 目录无效或不支持分卷文件");
201
+ const count = bytes.readUInt16LE(end + 10);
202
+ const size = bytes.readUInt32LE(end + 12);
203
+ let cursor = bytes.readUInt32LE(end + 16);
204
+ if (count > 2000 || count !== bytes.readUInt16LE(end + 8) || cursor + size !== end)
205
+ throw new Error("ZIP 目录无效或文件过多");
206
+ const names = new Set();
207
+ let total = 0;
208
+ for (let index = 0; index < count; index++) {
209
+ if (cursor + 46 > end || bytes.readUInt32LE(cursor) !== 0x02014b50)
210
+ throw new Error("ZIP 中央目录无效");
211
+ const length = bytes.readUInt16LE(cursor + 28);
212
+ const next = cursor + 46 + length + bytes.readUInt16LE(cursor + 30) + bytes.readUInt16LE(cursor + 32);
213
+ if (next > end || bytes.readUInt16LE(cursor + 8) & 1)
214
+ throw new Error("ZIP 目录无效或文件已加密");
215
+ const name = bytes.subarray(cursor + 46, cursor + 46 + length).toString("hex");
216
+ if (names.has(name))
217
+ throw new Error("ZIP 包含重复路径");
218
+ names.add(name);
219
+ total += bytes.readUInt32LE(cursor + 24);
220
+ if (total > 30 * 1024 * 1024)
221
+ throw new Error("ZIP 解压后不能超过 30 MiB");
222
+ cursor = next;
223
+ }
224
+ if (cursor !== end)
225
+ throw new Error("ZIP 中央目录无效");
226
+ return count;
227
+ }
228
+ const CRC32_TABLE = Uint32Array.from({ length: 256 }, (_, index) => {
229
+ let value = index;
230
+ for (let bit = 0; bit < 8; bit++)
231
+ value = (value >>> 1) ^ (value & 1 ? 0xedb88320 : 0);
232
+ return value >>> 0;
233
+ });
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.request = exports.Request = void 0;
4
+ const cli_apm_1 = require("./tracker/cli-apm");
5
+ class Request {
6
+ request;
7
+ constructor(request = fetch) {
8
+ this.request = request;
9
+ }
10
+ async fetch(options) {
11
+ const method = options.method ?? "GET";
12
+ const url = new URL(options.url);
13
+ if (url.protocol !== "https:" &&
14
+ !(url.protocol === "http:" && ["localhost", "127.0.0.1"].includes(url.hostname)))
15
+ throw new Error("HTTP 请求必须使用 HTTPS(本机联调除外)");
16
+ for (const [key, value] of Object.entries(options.data?.query ?? {})) {
17
+ if (value !== undefined && value !== null)
18
+ url.searchParams.set(key, String(value));
19
+ }
20
+ if (method === "GET") {
21
+ for (const [key, value] of Object.entries(options.data?.params ?? {})) {
22
+ if (value !== undefined && value !== null)
23
+ url.searchParams.set(key, String(value));
24
+ }
25
+ }
26
+ const body = method === "GET" || options.data?.body === undefined
27
+ ? undefined
28
+ : JSON.stringify(options.data.body);
29
+ const headers = { ...options.headers };
30
+ if (body && !Object.keys(headers).some((key) => key.toLowerCase() === "content-type")) {
31
+ headers["Content-Type"] = "application/json";
32
+ }
33
+ let response;
34
+ try {
35
+ response = await this.request(url, {
36
+ method,
37
+ headers,
38
+ body,
39
+ signal: AbortSignal.timeout(20_000),
40
+ });
41
+ }
42
+ catch (error) {
43
+ (0, cli_apm_1.reportCliHttpError)({ method, url: url.toString() }, error);
44
+ throw new Error("HTTP 请求失败", { cause: error });
45
+ }
46
+ if (response.status >= 400) {
47
+ (0, cli_apm_1.reportCliHttpError)({ method, url: url.toString(), status_code: response.status });
48
+ }
49
+ try {
50
+ return {
51
+ status: response.status,
52
+ data: (await response.json()),
53
+ headers: response.headers,
54
+ };
55
+ }
56
+ catch (error) {
57
+ (0, cli_apm_1.reportCliHttpError)({ method, url: url.toString(), status_code: response.status }, error);
58
+ throw new Error("HTTP 响应格式无效", { cause: error });
59
+ }
60
+ }
61
+ handleHttpStatus(response) {
62
+ if (response.status >= 200 && response.status < 300)
63
+ return response.data;
64
+ throw new Error(`HTTP 错误: ${response.status}`);
65
+ }
66
+ handleCode(data) {
67
+ if (data.code === 0)
68
+ return data;
69
+ throw new Error("业务请求失败");
70
+ }
71
+ }
72
+ exports.Request = Request;
73
+ exports.request = new Request();
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.printHorizontalTable = printHorizontalTable;
7
+ const cli_table3_1 = __importDefault(require("cli-table3"));
8
+ /**
9
+ * 打印水平方向的表格
10
+ *
11
+ * @param head 表头
12
+ * @param data 行数据,每行是一个数组
13
+ * @param options 透传给 cli-table3 的配置
14
+ */
15
+ function printHorizontalTable(head, data = [], options) {
16
+ if (!data?.length) {
17
+ console.log("列表数据为空");
18
+ }
19
+ const table = new cli_table3_1.default({
20
+ head,
21
+ style: { head: ["yellow"] },
22
+ colAligns: new Array(head.length).fill("center"),
23
+ ...options,
24
+ });
25
+ data.forEach((item) => {
26
+ table.push(item);
27
+ });
28
+ console.log(table.toString());
29
+ }
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toHump = void 0;
4
+ const toHump = (str) => str.replace(/\-(\w)/g, (letter) => letter[1].toUpperCase());
5
+ exports.toHump = toHump;
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AI_MEASUREMENT_NAME = void 0;
4
+ exports.initAiApm = initAiApm;
5
+ exports.reportAiSessionRun = reportAiSessionRun;
6
+ exports.reportAiTurnCodeGenerated = reportAiTurnCodeGenerated;
7
+ exports.reportAiSessionComplete = reportAiSessionComplete;
8
+ exports.reportAiCodeChangeApplied = reportAiCodeChangeApplied;
9
+ exports.reportAiCodeChangeRejected = reportAiCodeChangeRejected;
10
+ exports.reportAiBulkResolve = reportAiBulkResolve;
11
+ exports.reportAiMessageSent = reportAiMessageSent;
12
+ const apm_1 = require("./apm");
13
+ /**
14
+ * AI 助手服务统一点位:
15
+ * measurement_name 固定为 redminitool_ai_tool_usage,
16
+ * 通过 measurement_data.event 区分事件类型:
17
+ * - session_run : AI 对话请求开始(= 出码服务使用次数)
18
+ * - turn_code_generated : 每轮 AI 出码行数统计
19
+ * - session_complete : 整次对话完成汇总
20
+ * - code_change_applied : 用户接受单个 AI 代码变更(接受率分子)
21
+ * - code_change_rejected : 用户拒绝单个 AI 代码变更(接受率分母)
22
+ * - bulk_resolve : 用户批量接受/拒绝所有变更
23
+ * - message_sent : 前端用户发送消息(来自浏览器端 APM 代理)
24
+ */
25
+ exports.AI_MEASUREMENT_NAME = "redminitool_ai_tool_usage";
26
+ let _apm = null;
27
+ function initAiApm(options = {}) {
28
+ if (!_apm)
29
+ _apm = new apm_1.APM();
30
+ _apm.setContext(options.userId || "", {});
31
+ }
32
+ function reportAiSessionRun(meta) {
33
+ if (!_apm)
34
+ return;
35
+ _apm.report(exports.AI_MEASUREMENT_NAME, {
36
+ event: "session_run",
37
+ session_id: meta.session_id,
38
+ ...(meta.model ? { model: meta.model } : {}),
39
+ ...(meta.dev_mode ? { dev_mode: meta.dev_mode } : {}),
40
+ ...(typeof meta.is_new_session === "number" ? { is_new_session: meta.is_new_session } : {}),
41
+ ...(typeof meta.message_length === "number" ? { message_length: meta.message_length } : {}),
42
+ });
43
+ }
44
+ function reportAiTurnCodeGenerated(meta) {
45
+ if (!_apm)
46
+ return;
47
+ if (!meta.files_changed && !meta.lines_added && !meta.lines_removed)
48
+ return;
49
+ _apm.report(exports.AI_MEASUREMENT_NAME, {
50
+ event: "turn_code_generated",
51
+ ...(meta.session_id ? { session_id: meta.session_id } : {}),
52
+ ...(meta.model ? { model: meta.model } : {}),
53
+ ...(meta.dev_mode ? { dev_mode: meta.dev_mode } : {}),
54
+ ...(typeof meta.files_changed === "number" ? { files_changed: meta.files_changed } : {}),
55
+ ...(typeof meta.lines_added === "number" ? { lines_added: meta.lines_added } : {}),
56
+ ...(typeof meta.lines_removed === "number" ? { lines_removed: meta.lines_removed } : {}),
57
+ });
58
+ }
59
+ function reportAiSessionComplete(meta) {
60
+ if (!_apm)
61
+ return;
62
+ _apm.report(exports.AI_MEASUREMENT_NAME, {
63
+ event: "session_complete",
64
+ session_id: meta.session_id,
65
+ ...(meta.model ? { model: meta.model } : {}),
66
+ ...(meta.dev_mode ? { dev_mode: meta.dev_mode } : {}),
67
+ duration_ms: meta.duration_ms,
68
+ status: meta.status,
69
+ ...(typeof meta.total_files_changed === "number"
70
+ ? { total_files_changed: meta.total_files_changed }
71
+ : {}),
72
+ ...(typeof meta.total_lines_added === "number"
73
+ ? { total_lines_added: meta.total_lines_added }
74
+ : {}),
75
+ ...(typeof meta.total_lines_removed === "number"
76
+ ? { total_lines_removed: meta.total_lines_removed }
77
+ : {}),
78
+ ...(meta.error_type ? { error_type: meta.error_type } : {}),
79
+ });
80
+ }
81
+ function reportAiCodeChangeApplied(meta) {
82
+ if (!_apm)
83
+ return;
84
+ _apm.report(exports.AI_MEASUREMENT_NAME, {
85
+ event: "code_change_applied",
86
+ ...(meta.session_id ? { session_id: meta.session_id } : {}),
87
+ ...(meta.tool_name ? { tool_name: meta.tool_name } : {}),
88
+ });
89
+ }
90
+ function reportAiCodeChangeRejected(meta) {
91
+ if (!_apm)
92
+ return;
93
+ _apm.report(exports.AI_MEASUREMENT_NAME, {
94
+ event: "code_change_rejected",
95
+ ...(meta.session_id ? { session_id: meta.session_id } : {}),
96
+ ...(meta.tool_name ? { tool_name: meta.tool_name } : {}),
97
+ });
98
+ }
99
+ function reportAiBulkResolve(meta) {
100
+ if (!_apm)
101
+ return;
102
+ _apm.report(exports.AI_MEASUREMENT_NAME, {
103
+ event: "bulk_resolve",
104
+ action: meta.action,
105
+ ...(meta.session_id ? { session_id: meta.session_id } : {}),
106
+ ...(typeof meta.files_count === "number" ? { files_count: meta.files_count } : {}),
107
+ });
108
+ }
109
+ function reportAiMessageSent(meta) {
110
+ if (!_apm)
111
+ return;
112
+ _apm.report(exports.AI_MEASUREMENT_NAME, {
113
+ event: "message_sent",
114
+ ...(meta.session_id ? { session_id: meta.session_id } : {}),
115
+ ...(meta.mode ? { mode: meta.mode } : {}),
116
+ ...(meta.model ? { model: meta.model } : {}),
117
+ ...(typeof meta.message_length === "number" ? { message_length: meta.message_length } : {}),
118
+ });
119
+ }
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.APM = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ /** 通用 APM 传输,不自动获取设备 ID、登录态或凭据。 */
6
+ class APM {
7
+ sessionId = (0, node_crypto_1.randomUUID)();
8
+ sequence = 0;
9
+ appVersion = "unknown";
10
+ userId = "";
11
+ base = {};
12
+ transport;
13
+ pending = new Set();
14
+ constructor(options = {}) {
15
+ const endpoint = options.endpoint ?? process.env.REDMINITOOL_APM_ENDPOINT;
16
+ this.transport = options.transport ?? (endpoint ? createHttpTransport(endpoint) : undefined);
17
+ }
18
+ setContext(userId, base = {}) {
19
+ this.userId = userId;
20
+ this.base = base;
21
+ }
22
+ setAppVersion(version) {
23
+ this.appVersion = version || "unknown";
24
+ }
25
+ report(measurement_name, measurement_data = {}) {
26
+ if (!this.transport)
27
+ return;
28
+ const safeData = Object.fromEntries(Object.entries({ ...this.base, ...measurement_data }).filter(([key]) => !/secret|token|password|credential|headers|body|message|stack|url/i.test(key)));
29
+ const event = {
30
+ context_nameTracker: "nodeT",
31
+ context_platform: process.platform,
32
+ context_appVersion: this.appVersion,
33
+ context_userId: this.userId,
34
+ context_sdkSessionId: this.sessionId,
35
+ context_sdkSeqId: ++this.sequence,
36
+ clientTime: Date.now(),
37
+ measurement_name,
38
+ measurement_data: safeData,
39
+ };
40
+ let task;
41
+ task = Promise.resolve()
42
+ .then(() => this.transport?.([event]))
43
+ .then(() => undefined, () => undefined)
44
+ .finally(() => this.pending.delete(task));
45
+ this.pending.add(task);
46
+ }
47
+ async flush() {
48
+ await Promise.allSettled([...this.pending]);
49
+ }
50
+ }
51
+ exports.APM = APM;
52
+ function createHttpTransport(endpoint) {
53
+ let url;
54
+ try {
55
+ url = new URL(endpoint);
56
+ }
57
+ catch {
58
+ return undefined;
59
+ }
60
+ if (url.protocol !== "https:" &&
61
+ !(url.protocol === "http:" && ["localhost", "127.0.0.1"].includes(url.hostname)))
62
+ return undefined;
63
+ return async (payload) => {
64
+ const response = await fetch(url, {
65
+ method: "POST",
66
+ headers: { "content-type": "application/json", batch: "true", "biz-type": "apm_fe" },
67
+ body: JSON.stringify(payload),
68
+ signal: AbortSignal.timeout(1500),
69
+ });
70
+ if (!response.ok)
71
+ throw new Error(`APM HTTP ${response.status}`);
72
+ };
73
+ }