@ats-cx/cx-cli 0.1.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.
Files changed (57) hide show
  1. package/.env.example +8 -0
  2. package/README.md +119 -0
  3. package/bin/cx-cli.cjs +90 -0
  4. package/config/apm-provider.json +7 -0
  5. package/config/apm-provider.template.jsonc +48 -0
  6. package/config/diagnostic-rules.json +54 -0
  7. package/config/diagnostic-rules.template.jsonc +82 -0
  8. package/config/event-semantics.json +828 -0
  9. package/config/event-semantics.template.jsonc +13 -0
  10. package/config/toolkit.json +3 -0
  11. package/dist/apm-help.d.ts +5 -0
  12. package/dist/apm-help.js +108 -0
  13. package/dist/apm-output.d.ts +49 -0
  14. package/dist/apm-output.js +115 -0
  15. package/dist/budget.d.ts +14 -0
  16. package/dist/budget.js +16 -0
  17. package/dist/cli.d.ts +20 -0
  18. package/dist/cli.js +596 -0
  19. package/dist/context.d.ts +12 -0
  20. package/dist/context.js +20 -0
  21. package/dist/contract.d.ts +11 -0
  22. package/dist/contract.js +4 -0
  23. package/dist/errors.d.ts +4 -0
  24. package/dist/errors.js +8 -0
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.js +8 -0
  27. package/dist/init.d.ts +17 -0
  28. package/dist/init.js +194 -0
  29. package/dist/output.d.ts +7 -0
  30. package/dist/output.js +7 -0
  31. package/dist/project-pull.d.ts +32 -0
  32. package/dist/project-pull.js +81 -0
  33. package/dist/report.d.ts +3 -0
  34. package/dist/report.js +42 -0
  35. package/dist/skills.d.ts +12 -0
  36. package/dist/skills.js +430 -0
  37. package/dist/tools/apm-tools.d.ts +115 -0
  38. package/dist/tools/apm-tools.js +329 -0
  39. package/dist/tools/conclusion-tools.d.ts +24 -0
  40. package/dist/tools/conclusion-tools.js +61 -0
  41. package/dist/tools/create-run.d.ts +61 -0
  42. package/dist/tools/create-run.js +273 -0
  43. package/dist/tools/log-tools.d.ts +68 -0
  44. package/dist/tools/log-tools.js +116 -0
  45. package/dist/tools/project-tools.d.ts +82 -0
  46. package/dist/tools/project-tools.js +139 -0
  47. package/dist/workspace.d.ts +51 -0
  48. package/dist/workspace.js +104 -0
  49. package/package.json +37 -0
  50. package/skills/apm-query/SKILL.md +304 -0
  51. package/skills/apm-query/agents/openai.yaml +10 -0
  52. package/skills/cx-cli-setup/SKILL.md +120 -0
  53. package/skills/cx-cli-setup/agents/openai.yaml +11 -0
  54. package/skills/editor-diagnostic/SKILL.md +255 -0
  55. package/skills/editor-diagnostic/agents/openai.yaml +13 -0
  56. package/skills/semantics-curation/SKILL.md +151 -0
  57. package/skills/semantics-curation/agents/openai.yaml +12 -0
@@ -0,0 +1,329 @@
1
+ import { ApmRequestError, assertColumnName, assertSafeValue, createApmClient, createApmConfigError, fetchLiveProject, FIXED_REQUEST_KEYS, isSiteHost, isApmConfigured, LOG_TYPES, normalizeLogTime, ORDERS, resolveUserId, } from "@ats-cx/cx-core";
2
+ import { z } from "zod";
3
+ import { buildQueryPreview, createProjectFileName, createQueryFileName, resolveApmOutputFile, writeQueryDump, writeRawApmFile, } from "../apm-output.js";
4
+ import { defineTool } from "../contract.js";
5
+ function requireApmProvider(config) {
6
+ if (!isApmConfigured(config.apmProviderState)) {
7
+ throw createApmConfigError(config.apmProviderState);
8
+ }
9
+ if (config.apmProvider === null) {
10
+ throw new ApmRequestError("invalid_config", { message: "apm 配置状态与 provider 不一致" });
11
+ }
12
+ return config.apmProvider;
13
+ }
14
+ function stringWithValidation(validate) {
15
+ return z.string().superRefine((value, context) => {
16
+ try {
17
+ validate(value);
18
+ }
19
+ catch (error) {
20
+ context.addIssue({
21
+ code: z.ZodIssueCode.custom,
22
+ message: error instanceof Error ? error.message : String(error),
23
+ });
24
+ }
25
+ });
26
+ }
27
+ const safeValueSchema = stringWithValidation(value => assertSafeValue("值", value));
28
+ const emailSchema = stringWithValidation(value => {
29
+ assertSafeValue("email", value);
30
+ if (!value.includes("@")) {
31
+ throw new Error("email 必须包含 @");
32
+ }
33
+ });
34
+ const phoneSchema = stringWithValidation(value => {
35
+ assertSafeValue("phone", value);
36
+ if (!/^1[3-9]\d{9}$/.test(value)) {
37
+ throw new Error("phone 必须是 1 开头的 11 位中国大陆手机号");
38
+ }
39
+ });
40
+ const hostSchema = stringWithValidation(value => {
41
+ assertSafeValue("host", value);
42
+ if (!isSiteHost(value)) {
43
+ throw new Error("传日志 host 列的完整域名,如 www.example.com");
44
+ }
45
+ });
46
+ function logTimeSchema(edge) {
47
+ return z.string().transform((value, context) => {
48
+ try {
49
+ return normalizeLogTime(value, edge);
50
+ }
51
+ catch (error) {
52
+ context.addIssue({
53
+ code: z.ZodIssueCode.custom,
54
+ message: error instanceof Error ? error.message : String(error),
55
+ });
56
+ return z.NEVER;
57
+ }
58
+ });
59
+ }
60
+ const whereSchema = z.array(z.string()).transform((entries, context) => {
61
+ const where = {};
62
+ entries.forEach((entry, index) => {
63
+ const separator = entry.indexOf("=");
64
+ const column = separator >= 0 ? entry.slice(0, separator) : entry;
65
+ const value = separator >= 0 ? entry.slice(separator + 1) : "";
66
+ try {
67
+ assertColumnName(column);
68
+ if (column === "type") {
69
+ throw new Error("type 不可用于 --where:接口把请求键 type 当日志类型(即 --type)而非列名,此列无法过滤;先落盘再用 jq 筛");
70
+ }
71
+ if (FIXED_REQUEST_KEYS.has(column)) {
72
+ throw new Error(`${column} 是固定请求键,不能通过 --where 覆盖`);
73
+ }
74
+ if (column === "data_name") {
75
+ throw new Error("data_name 不可用于 --where,请用 --event");
76
+ }
77
+ if (value.length === 0) {
78
+ throw new Error(`--where ${column}= 的值不能为空`);
79
+ }
80
+ assertSafeValue(`--where ${column}`, value);
81
+ if (Object.hasOwn(where, column)) {
82
+ throw new Error(`--where 同一列不能重复: ${column}`);
83
+ }
84
+ where[column] = value;
85
+ }
86
+ catch (error) {
87
+ context.addIssue({
88
+ code: z.ZodIssueCode.custom,
89
+ path: [index],
90
+ message: error instanceof Error ? error.message : String(error),
91
+ });
92
+ }
93
+ });
94
+ return where;
95
+ });
96
+ const queryInputSchema = z
97
+ .object({
98
+ type: z.enum(LOG_TYPES).optional(),
99
+ from: logTimeSchema("start"),
100
+ to: logTimeSchema("end").optional(),
101
+ order: z.enum(ORDERS).optional(),
102
+ project: safeValueSchema.optional(),
103
+ user: safeValueSchema.optional(),
104
+ collection: safeValueSchema.optional(),
105
+ session: safeValueSchema.optional(),
106
+ subType: safeValueSchema.optional(),
107
+ event: safeValueSchema.optional(),
108
+ app: safeValueSchema.optional(),
109
+ bizline: safeValueSchema.optional(),
110
+ item: safeValueSchema.optional(),
111
+ search: safeValueSchema.optional(),
112
+ where: whereSchema.optional(),
113
+ limit: z.number().int().min(1).optional(),
114
+ count: z.boolean().optional(),
115
+ preview: z.number().int().min(1).max(200).optional(),
116
+ full: z.boolean().optional(),
117
+ out: z.string().min(1).optional(),
118
+ run: z.string().min(1).optional(),
119
+ noFlush: z.boolean().optional(),
120
+ })
121
+ .superRefine((input, context) => {
122
+ if (input.to !== undefined && input.to < input.from) {
123
+ context.addIssue({ code: z.ZodIssueCode.custom, path: ["to"], message: "结束时间不能早于开始时间" });
124
+ }
125
+ if (input.item !== undefined && [input.project, input.user, input.collection].some(value => value !== undefined)) {
126
+ context.addIssue({
127
+ code: z.ZodIssueCode.custom,
128
+ path: ["item"],
129
+ message: "--item 与 --project / --user / --collection 互斥",
130
+ });
131
+ }
132
+ if (input.run !== undefined && input.out !== undefined) {
133
+ context.addIssue({ code: z.ZodIssueCode.custom, path: ["run"], message: "--run 与 --out 互斥" });
134
+ }
135
+ if (input.count === true) {
136
+ const conflicts = ["limit", "preview", "full", "out", "run"];
137
+ const provided = conflicts.filter(key => input[key] !== undefined && input[key] !== false);
138
+ if (provided.length > 0) {
139
+ context.addIssue({
140
+ code: z.ZodIssueCode.custom,
141
+ path: ["count"],
142
+ message: `--count 与 ${provided.map(key => `--${key}`).join(" / ")} 互斥`,
143
+ });
144
+ }
145
+ }
146
+ })
147
+ .transform(input => ({
148
+ ...input,
149
+ type: input.type ?? "user_behav",
150
+ order: input.order ?? "DESC",
151
+ preview: input.preview ?? 20,
152
+ count: input.count ?? false,
153
+ full: input.full ?? false,
154
+ noFlush: input.noFlush ?? false,
155
+ where: input.where ?? {},
156
+ }));
157
+ const resolveInputSchema = z
158
+ .object({
159
+ email: emailSchema.optional(),
160
+ phone: phoneSchema.optional(),
161
+ host: hostSchema.optional(),
162
+ })
163
+ .superRefine((input, context) => {
164
+ if ((input.email === undefined) === (input.phone === undefined)) {
165
+ context.addIssue({
166
+ code: z.ZodIssueCode.custom,
167
+ path: ["email"],
168
+ message: "--email 与 --phone 必须且只能二选一",
169
+ });
170
+ }
171
+ });
172
+ const projectInputSchema = z
173
+ .object({
174
+ projectId: z.string().regex(/^\d+$/, "projectId 只收数字"),
175
+ host: hostSchema.optional(),
176
+ out: z.string().min(1).optional(),
177
+ run: z.string().min(1).optional(),
178
+ })
179
+ .superRefine((input, context) => {
180
+ if (input.out !== undefined && input.run !== undefined) {
181
+ context.addIssue({ code: z.ZodIssueCode.custom, path: ["run"], message: "--run 与 --out 互斥" });
182
+ }
183
+ });
184
+ export function createApmQueryTool(deps = {}) {
185
+ return defineTool({
186
+ name: "apm_query",
187
+ description: "查询 APM 埋点日志并落盘完整结果,stdout 返回摘要与预览",
188
+ inputSchema: queryInputSchema,
189
+ handler: async (input, context) => {
190
+ const config = requireApmProvider(context.config);
191
+ const client = createApmClient(config, {
192
+ ...deps,
193
+ onProgress: deps.onProgress ?? (message => console.error(message)),
194
+ });
195
+ const params = {
196
+ type: input.type,
197
+ from: input.from,
198
+ to: input.to,
199
+ order: input.order,
200
+ project: input.project,
201
+ user: input.user,
202
+ collection: input.collection,
203
+ session: input.session,
204
+ subType: input.subType,
205
+ event: input.event,
206
+ app: input.app ?? config.defaults.app_id,
207
+ bizline: input.bizline ?? config.defaults.bizline_id,
208
+ where: input.where,
209
+ item: input.item,
210
+ search: input.search,
211
+ limit: input.limit,
212
+ flush: !input.noFlush,
213
+ };
214
+ if (input.count) {
215
+ const result = await client.count(params);
216
+ return { total: result.total };
217
+ }
218
+ const file = resolveApmOutputFile({
219
+ out: input.out,
220
+ run: input.run,
221
+ runsDir: context.config.runsDir,
222
+ outDir: context.config.outDir,
223
+ fileName: createQueryFileName(input.type),
224
+ });
225
+ try {
226
+ const result = await client.query(params);
227
+ const fetchedAt = new Date();
228
+ writeQueryDump(file, result.rows, {
229
+ fetchedAt,
230
+ request: result.request,
231
+ flush: result.flush,
232
+ total: result.total,
233
+ fetched: result.rows.length,
234
+ pagesFetched: result.pagesFetched,
235
+ warnings: result.warnings,
236
+ });
237
+ return buildQueryPreview({
238
+ rows: result.rows,
239
+ total: result.total,
240
+ pagesFetched: result.pagesFetched,
241
+ file,
242
+ order: input.order,
243
+ preview: input.preview,
244
+ full: input.full,
245
+ alreadySorted: input.limit !== undefined,
246
+ flush: result.flush,
247
+ });
248
+ }
249
+ catch (error) {
250
+ if (error instanceof ApmRequestError && error.kind === "incomplete" && error.partialRows !== undefined) {
251
+ writeQueryDump(file, error.partialRows, {
252
+ fetchedAt: new Date(),
253
+ request: error.request ?? { url: `${config.baseUrl}/api/log/query`, body: {} },
254
+ flush: error.flush ?? { attempted: !input.noFlush },
255
+ total: error.total ?? error.partialRows.length,
256
+ fetched: error.partialRows.length,
257
+ pagesFetched: error.pagesFetched ?? 0,
258
+ warnings: error.warnings ?? [],
259
+ });
260
+ throw Object.assign(error, { file });
261
+ }
262
+ throw error;
263
+ }
264
+ },
265
+ });
266
+ }
267
+ export function createApmFlushTool(deps = {}) {
268
+ return defineTool({
269
+ name: "apm_flush",
270
+ description: "让网页端把攒批未写入的事件落库",
271
+ inputSchema: z.object({}),
272
+ handler: async (_input, context) => {
273
+ const config = requireApmProvider(context.config);
274
+ const result = await createApmClient(config, deps).flush();
275
+ if (!result.ok) {
276
+ throw new ApmRequestError(result.reason ?? "invalid_response", {
277
+ status: result.status,
278
+ baseUrl: config.baseUrl,
279
+ });
280
+ }
281
+ return { flushed: true, message: result.message };
282
+ },
283
+ });
284
+ }
285
+ export function createApmResolveTool(deps = {}) {
286
+ return defineTool({
287
+ name: "apm_resolve",
288
+ description: "经网页端 portal 免登录路由把邮箱或手机号解析为 userId",
289
+ inputSchema: resolveInputSchema,
290
+ handler: async (input, context) => {
291
+ const config = requireApmProvider(context.config);
292
+ const host = input.host ?? config.defaults.host;
293
+ const value = input.email ?? input.phone;
294
+ const userId = await resolveUserId(config, { value, host }, deps);
295
+ return input.email !== undefined
296
+ ? { userId, email: input.email, host }
297
+ : { userId, phone: input.phone, host };
298
+ },
299
+ });
300
+ }
301
+ export function createApmProjectTool(deps = {}) {
302
+ return defineTool({
303
+ name: "apm_project",
304
+ description: "经网页端 portal 免登录路由拉取现网 project JSON 并原样落盘",
305
+ inputSchema: projectInputSchema,
306
+ handler: async (input, context) => {
307
+ const config = requireApmProvider(context.config);
308
+ const host = input.host ?? config.defaults.host;
309
+ const file = resolveApmOutputFile({
310
+ out: input.out,
311
+ run: input.run,
312
+ runsDir: context.config.runsDir,
313
+ outDir: context.config.outDir,
314
+ fileName: createProjectFileName(input.projectId),
315
+ });
316
+ const result = await fetchLiveProject(config, { projectId: input.projectId, host }, deps);
317
+ const bytes = writeRawApmFile(file, result.text);
318
+ if (!result.hasProject) {
319
+ console.error("警告: 返回体无 project 键(package 项目?),已原样落盘");
320
+ }
321
+ return { projectId: input.projectId, host, file, bytes, updatedDate: result.updatedDate };
322
+ },
323
+ });
324
+ }
325
+ export const apmQueryTool = createApmQueryTool();
326
+ export const apmResolveTool = createApmResolveTool();
327
+ export const apmProjectTool = createApmProjectTool();
328
+ export const apmFlushTool = createApmFlushTool();
329
+ //# sourceMappingURL=apm-tools.js.map
@@ -0,0 +1,24 @@
1
+ export declare const recordHypothesisTool: import("../contract.js").ToolDefinition<{
2
+ runId: string;
3
+ description: string;
4
+ confidence: "confirmed" | "likely" | "inconclusive";
5
+ evidenceRefs: string[];
6
+ missingEvidence: string[];
7
+ }, {
8
+ recorded: boolean;
9
+ count: number;
10
+ }>;
11
+ export declare const finalizeDiagnosisTool: import("../contract.js").ToolDefinition<{
12
+ runId: string;
13
+ result?: unknown;
14
+ }, {
15
+ ok: false;
16
+ errors: string[] | undefined;
17
+ reportPath?: undefined;
18
+ resultPath?: undefined;
19
+ } | {
20
+ ok: true;
21
+ reportPath: string;
22
+ resultPath: string;
23
+ errors?: undefined;
24
+ }>;
@@ -0,0 +1,61 @@
1
+ import { appendFileSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { z } from "zod";
4
+ import { loadRun, validateDiagnosisResult, writeStageStatus } from "@ats-cx/cx-core";
5
+ import { defineTool } from "../contract.js";
6
+ import { renderReport } from "../report.js";
7
+ function appendHypothesisLine(runDir, entry) {
8
+ const filePath = join(runDir, "artifacts", "hypotheses.jsonl");
9
+ appendFileSync(filePath, JSON.stringify({ ts: new Date().toISOString(), ...entry }) + "\n");
10
+ const content = readFileSync(filePath, "utf8").trim();
11
+ return content ? content.split("\n").length : 0;
12
+ }
13
+ export const recordHypothesisTool = defineTool({
14
+ name: "record_hypothesis",
15
+ description: "记录诊断假设、置信度、证据引用与缺失证据",
16
+ inputSchema: z.object({
17
+ runId: z.string().min(1),
18
+ description: z.string().min(1),
19
+ confidence: z.enum(["confirmed", "likely", "inconclusive"]),
20
+ evidenceRefs: z.array(z.string()),
21
+ missingEvidence: z.array(z.string()).default([]),
22
+ }),
23
+ handler: async (input, context) => {
24
+ const run = loadRun(context.config.runsDir, input.runId);
25
+ const count = appendHypothesisLine(run.runDir, {
26
+ description: input.description,
27
+ confidence: input.confidence,
28
+ evidenceRefs: input.evidenceRefs,
29
+ missingEvidence: input.missingEvidence,
30
+ });
31
+ return { recorded: true, count };
32
+ },
33
+ });
34
+ export const finalizeDiagnosisTool = defineTool({
35
+ name: "finalize_diagnosis",
36
+ description: "校验并提交诊断结论,生成 result.json 与六段式 report.md",
37
+ inputSchema: z.object({
38
+ runId: z.string().min(1),
39
+ result: z.unknown(),
40
+ }),
41
+ handler: async (input, context) => {
42
+ const run = loadRun(context.config.runsDir, input.runId);
43
+ const raw = typeof input.result === "object" && input.result !== null
44
+ ? { ...input.result }
45
+ : {};
46
+ raw.runId = input.runId;
47
+ raw.projectId = run.input.projectId;
48
+ const validation = validateDiagnosisResult(raw);
49
+ if (!validation.ok) {
50
+ return { ok: false, errors: validation.errors };
51
+ }
52
+ const result = validation.result;
53
+ const resultPath = join(run.runDir, "result.json");
54
+ writeFileSync(resultPath, JSON.stringify(result, null, 2));
55
+ const reportPath = join(run.runDir, "report.md");
56
+ writeFileSync(reportPath, renderReport(run.runDir, result, run.input.complaint));
57
+ writeStageStatus(run.runDir, "finalize", "completed");
58
+ return { ok: true, reportPath, resultPath };
59
+ },
60
+ });
61
+ //# sourceMappingURL=conclusion-tools.js.map
@@ -0,0 +1,61 @@
1
+ import type { SuspiciousSignal } from "@ats-cx/cx-core";
2
+ export interface SemanticsCoverageEntry {
3
+ eventName: string;
4
+ count: number;
5
+ /** 解析器实际落的类别:inferred 条目带它即隐含风险级;unknown 的 error_payload 修补件在此显形为 "error"。 */
6
+ category: string;
7
+ /** 该事件名全部出现的 payload 键名并集(跨 session 聚合),排序后输出保证产物 diff 稳定。 */
8
+ payloadKeys: string[];
9
+ }
10
+ export interface SemanticsCoverage {
11
+ /** 命中显式表的去重事件名数(事件总次数由既有 eventCount 承担)。 */
12
+ explicitEventCount: number;
13
+ inferred: SemanticsCoverageEntry[];
14
+ unknown: SemanticsCoverageEntry[];
15
+ }
16
+ export interface CaseBrief {
17
+ projectId: string;
18
+ complaint: string;
19
+ sessionCount: number;
20
+ sessionIds: string[];
21
+ eventCount: number;
22
+ /**
23
+ * 日志实际覆盖的时间跨度(取自事件 logTime)。对照客诉发生时间:跨度盖不住事发时段,说明
24
+ * 事发会话根本没进日志源,只能记 missingEvidence,不能拿客服/研发的复现会话当客户行为分析。
25
+ */
26
+ logTimeRange: {
27
+ earliest: string | null;
28
+ latest: string | null;
29
+ };
30
+ /**
31
+ * 两钟差值(logTime − receivedTime 的中位数,分钟)。整小时 ≈ 时区差,零碎 ≈ 钟漂或上报缓冲;
32
+ * 判读口径见 apm-query skill「时间窗」。日志源无 received_time 时 medianOffsetMinutes 为 null 并降级提示。
33
+ */
34
+ clockSkew: {
35
+ medianOffsetMinutes: number | null;
36
+ hint: string | null;
37
+ };
38
+ projectVersions: string[];
39
+ suspiciousSignals: SuspiciousSignal[];
40
+ semanticsCoverage: SemanticsCoverage;
41
+ sourceRepos: Array<{
42
+ id: string;
43
+ reachable: boolean;
44
+ }>;
45
+ missingEvidence: string[];
46
+ logSource: {
47
+ source: "apm" | "file";
48
+ file?: string;
49
+ filter?: string;
50
+ matched: number;
51
+ other: number;
52
+ };
53
+ }
54
+ export declare const createRunTool: import("../contract.js").ToolDefinition<{
55
+ projectId: string;
56
+ complaint: string;
57
+ logsFile?: string | undefined;
58
+ }, {
59
+ runId: string;
60
+ brief: CaseBrief;
61
+ }>;