@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,273 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { z } from "zod";
4
+ import { ApmRequestError, createApmConfigError, isApmConfigured, createFileLogProvider, createRunDir, createSemanticsResolver, diffProjectSnapshots, normalizeProjectSnapshot, runDiagnosticRules, writeArtifact, writeStageStatus, } from "@ats-cx/cx-core";
5
+ import { defineTool } from "../contract.js";
6
+ import { UserFacingError } from "../errors.js";
7
+ const inputSchema = z.object({
8
+ projectId: z.string().min(1),
9
+ complaint: z.string().default(""),
10
+ logsFile: z.string().min(1).optional(),
11
+ });
12
+ function selectCoverageCategory(left, right) {
13
+ if (left === right)
14
+ return left;
15
+ if (left === "error" || right === "error")
16
+ return "error";
17
+ if (left === "unknown")
18
+ return right;
19
+ if (right === "unknown")
20
+ return left;
21
+ return [left, right].sort()[0];
22
+ }
23
+ /**
24
+ * 按 (eventName, source bucket) 聚合三级语义解析结果(spec 组件 B):inferred 与 unknown 就是待策展队列。
25
+ * 类别也只在 bucket 内合并;error 优先于 unknown,其他冲突按稳定规则选择,避免日志顺序改变产物。
26
+ */
27
+ function buildSemanticsCoverage(sessions) {
28
+ const explicitNames = new Set();
29
+ const pending = new Map();
30
+ for (const session of sessions) {
31
+ for (const event of session.events) {
32
+ if (event.semantics.source?.explicit) {
33
+ explicitNames.add(event.eventName);
34
+ continue;
35
+ }
36
+ const inferred = event.semantics.source?.inferred === true;
37
+ const buckets = pending.get(event.eventName) ?? new Map();
38
+ const entry = buckets.get(inferred) ?? {
39
+ count: 0,
40
+ category: event.semantics.category,
41
+ payloadKeys: new Set(),
42
+ };
43
+ entry.count += 1;
44
+ entry.category = selectCoverageCategory(entry.category, event.semantics.category);
45
+ for (const key of Object.keys(event.payload)) {
46
+ entry.payloadKeys.add(key);
47
+ }
48
+ buckets.set(inferred, entry);
49
+ pending.set(event.eventName, buckets);
50
+ }
51
+ }
52
+ const collect = (inferred) => [...pending.entries()]
53
+ .flatMap(([eventName, buckets]) => {
54
+ const value = buckets.get(inferred);
55
+ return value ? [[eventName, value]] : [];
56
+ })
57
+ .map(([eventName, value]) => ({
58
+ eventName,
59
+ count: value.count,
60
+ category: value.category,
61
+ payloadKeys: [...value.payloadKeys].sort(),
62
+ }))
63
+ // count 降序、同 count 按名升序;不截断(ADR 0002 判据 2:不替 Agent 做范围裁剪)
64
+ .sort((left, right) => right.count - left.count || left.eventName.localeCompare(right.eventName));
65
+ return {
66
+ explicitEventCount: explicitNames.size,
67
+ inferred: collect(true),
68
+ unknown: collect(false),
69
+ };
70
+ }
71
+ const CLOCK_SKEW_HINT_THRESHOLD_MINUTES = 30;
72
+ const WHOLE_HOUR_TOLERANCE_MINUTES = 5;
73
+ /**
74
+ * 计算两钟中位差并给出一行判读提示。只在 brief 出现(ADR 0005):
75
+ * timeline/排序/证据引用一律不用 receivedTime。
76
+ */
77
+ function buildClockSkew(events) {
78
+ const offsets = events
79
+ .filter(event => event.logTimestamp > 0 && event.receivedTimestamp > 0)
80
+ .map(event => (event.logTimestamp - event.receivedTimestamp) / 60_000)
81
+ .sort((left, right) => left - right);
82
+ if (offsets.length === 0) {
83
+ return { medianOffsetMinutes: null, hint: "日志源无 received_time,两钟差值判读不可用" };
84
+ }
85
+ const median = Math.round(offsets[Math.floor(offsets.length / 2)]);
86
+ if (Math.abs(median) < CLOCK_SKEW_HINT_THRESHOLD_MINUTES) {
87
+ return { medianOffsetMinutes: median, hint: null };
88
+ }
89
+ const remainder = Math.abs(median) % 60;
90
+ const nearWholeHour = remainder <= WHOLE_HOUR_TOLERANCE_MINUTES || remainder >= 60 - WHOLE_HOUR_TOLERANCE_MINUTES;
91
+ const sign = median > 0 ? "+" : "-";
92
+ const hint = nearWholeHour
93
+ ? `log_time 与 received_time 中位差约 ${sign}${Math.round(Math.abs(median) / 60)} 小时(接近整小时,疑似时区差)`
94
+ : `log_time 与 received_time 中位差约 ${sign}${Math.abs(median)} 分钟(零碎差值,疑似钟漂或上报缓冲)`;
95
+ return { medianOffsetMinutes: median, hint };
96
+ }
97
+ function buildLogTimeRange(events) {
98
+ let earliest = null;
99
+ let latest = null;
100
+ for (const event of events) {
101
+ if (!earliest || event.logTimestamp < earliest.logTimestamp)
102
+ earliest = event;
103
+ if (!latest || event.logTimestamp > latest.logTimestamp)
104
+ latest = event;
105
+ }
106
+ return { earliest: earliest?.logTime ?? null, latest: latest?.logTime ?? null };
107
+ }
108
+ function buildLogSourceFilter(body) {
109
+ if (!body)
110
+ return undefined;
111
+ const parts = ["type", "project_id", "log_time_start", "log_time_end"].flatMap(key => {
112
+ const value = body[key];
113
+ return typeof value === "string" || typeof value === "number" ? [`${key}=${value}`] : [];
114
+ });
115
+ return parts.length > 0 ? parts.join(" / ") : undefined;
116
+ }
117
+ function getMetaRequestBody(meta) {
118
+ if (typeof meta !== "object" || meta === null || Array.isArray(meta))
119
+ return undefined;
120
+ const request = meta.request;
121
+ if (typeof request !== "object" || request === null || Array.isArray(request))
122
+ return undefined;
123
+ const body = request.body;
124
+ return typeof body === "object" && body !== null && !Array.isArray(body)
125
+ ? body
126
+ : undefined;
127
+ }
128
+ function buildEvidenceIndex(sessions, diffs, history, signals) {
129
+ return {
130
+ logs: sessions.flatMap(session => session.events.map(event => ({
131
+ evidenceRef: `log:${event.sessionId}:${event.originalIndex}`,
132
+ sessionId: event.sessionId,
133
+ eventName: event.eventName,
134
+ logTime: event.logTime,
135
+ }))),
136
+ diffs: diffs.map(diff => ({
137
+ evidenceRef: `diff:${diff.before}-${diff.after}`,
138
+ before: diff.before,
139
+ after: diff.after,
140
+ summary: diff.summary,
141
+ })),
142
+ snapshots: history.map(item => ({
143
+ evidenceRef: `snapshot:${item.version}`,
144
+ version: item.version,
145
+ filePath: item.filePath,
146
+ })),
147
+ signals: signals.map(signal => ({
148
+ evidenceRef: `signal:${signal.ruleId}:${signal.eventName}:${signal.logTime}`,
149
+ ruleId: signal.ruleId,
150
+ severity: signal.severity,
151
+ eventName: signal.eventName,
152
+ logTime: signal.logTime,
153
+ })),
154
+ };
155
+ }
156
+ export const createRunTool = defineTool({
157
+ name: "create_run",
158
+ description: "创建诊断 run:拉取日志与项目快照、预计算语义标注/diff/可疑信号,返回 case 摘要",
159
+ inputSchema,
160
+ handler: async (input, context) => {
161
+ const complaint = input.complaint ?? "";
162
+ if (input.logsFile === undefined &&
163
+ context.config.logSource.type === "apm" &&
164
+ !isApmConfigured(context.config.apmProviderState)) {
165
+ throw createApmConfigError(context.config.apmProviderState);
166
+ }
167
+ const run = createRunDir(context.config.runsDir, { projectId: input.projectId, complaint });
168
+ writeFileSync(join(run.runDir, "config.snapshot.json"), JSON.stringify({ configDir: context.config.configDir, sourceRepos: context.sourceRepos }, null, 2));
169
+ writeFileSync(join(run.runDir, "artifacts", "hypotheses.jsonl"), "");
170
+ writeStageStatus(run.runDir, "init_run", "completed");
171
+ const missingEvidence = [];
172
+ // collect_context(spec §11:provider 失败不摧毁 run——标记失败阶段后抛出,已写产物保留)
173
+ const sourceVersions = context.sourceProvider.snapshotVersions();
174
+ writeFileSync(join(run.runDir, "source-versions.json"), JSON.stringify(sourceVersions, null, 2));
175
+ let logs;
176
+ let history;
177
+ try {
178
+ const logProvider = input.logsFile ? createFileLogProvider(input.logsFile) : context.logProvider;
179
+ logs = await logProvider.fetch(input.projectId);
180
+ history = await context.projectProvider.loadHistory(input.projectId);
181
+ }
182
+ catch (error) {
183
+ const detail = error instanceof ApmRequestError && error.kind === "incomplete"
184
+ ? `${error.message};用 apm query --from/--to --out + run new --logs 收窄`
185
+ : error instanceof Error
186
+ ? error.message
187
+ : String(error);
188
+ writeStageStatus(run.runDir, "collect_context", "failed", detail);
189
+ throw error;
190
+ }
191
+ if (history.length === 0) {
192
+ const message = [
193
+ `错误: 未找到项目 ${input.projectId} 的本地快照(${join(context.config.projectHistoryDir, input.projectId)}/ 为空或不存在)。`,
194
+ "请先拉取项目数据:",
195
+ ` cx-cli project pull ${input.projectId} --history`,
196
+ ].join("\n");
197
+ writeStageStatus(run.runDir, "collect_context", "failed", message);
198
+ throw new UserFacingError(message);
199
+ }
200
+ for (const version of sourceVersions) {
201
+ if (!version.reachable) {
202
+ missingEvidence.push(`源码仓库 ${version.repoId} 不可达,源码级证据缺失`);
203
+ }
204
+ }
205
+ const projectIdMatch = logs.normalized.events.reduce((counts, event) => {
206
+ counts[event.projectId === input.projectId ? "matched" : "other"] += 1;
207
+ return counts;
208
+ }, { matched: 0, other: 0 });
209
+ if (projectIdMatch.other > 0) {
210
+ console.error(`警告: ${projectIdMatch.other} 行 project_id 与 --projectId 不一致(--logs 文件可能来自 --item 三列 OR 或传错文件)`);
211
+ }
212
+ if (logs.normalized.eventCount === 0) {
213
+ if (logs.provenance.source === "file") {
214
+ missingEvidence.push("日志源无该项目事件;文件里无该项目事件,核对 project_id");
215
+ }
216
+ else if (logs.provenance.flush.attempted && logs.provenance.flush.ok === true) {
217
+ missingEvidence.push("日志源无该项目事件;或用 apm query --item <id> 对照三列 OR 是否命中");
218
+ }
219
+ else {
220
+ missingEvidence.push("日志源无该项目事件;同步失败,先 apm flush 再重跑 run new");
221
+ }
222
+ }
223
+ writeArtifact(run.runDir, "logs.source.json", { ...logs.provenance, projectIdMatch });
224
+ writeStageStatus(run.runDir, "collect_context", "completed");
225
+ // precompute_evidence
226
+ writeArtifact(run.runDir, "logs.normalized.json", logs.normalized);
227
+ const resolveSemantics = createSemanticsResolver(context.config.eventSemantics);
228
+ const annotatedSessions = logs.normalized.sessions.map(session => ({
229
+ ...session,
230
+ events: session.events.map((event) => ({ ...event, semantics: resolveSemantics(event) })),
231
+ }));
232
+ writeArtifact(run.runDir, "sessions.annotated.json", annotatedSessions);
233
+ const signals = annotatedSessions.flatMap(session => runDiagnosticRules(session.events, context.config.diagnosticRules));
234
+ writeArtifact(run.runDir, "signals.json", signals);
235
+ const historyIndex = history.map(item => ({ version: item.version, filePath: item.filePath }));
236
+ writeArtifact(run.runDir, "project-history.index.json", historyIndex);
237
+ const diffs = [];
238
+ for (let index = 0; index + 1 < history.length; index += 1) {
239
+ const diff = diffProjectSnapshots(history[index].data, history[index + 1].data);
240
+ writeArtifact(run.runDir, `project-diff.${history[index].version}-${history[index + 1].version}.json`, diff);
241
+ diffs.push({ before: history[index].version, after: history[index + 1].version, summary: diff.summary });
242
+ }
243
+ writeArtifact(run.runDir, "project-diffs.index.json", diffs);
244
+ writeArtifact(run.runDir, "evidence.index.json", buildEvidenceIndex(annotatedSessions, diffs, historyIndex, signals));
245
+ writeArtifact(run.runDir, "project.latest.normalized.json", normalizeProjectSnapshot(history[history.length - 1].data));
246
+ writeStageStatus(run.runDir, "precompute_evidence", "completed");
247
+ const logSourceFilter = buildLogSourceFilter(logs.provenance.request?.body ?? getMetaRequestBody(logs.provenance.meta));
248
+ const allEvents = annotatedSessions.flatMap(session => session.events);
249
+ const brief = {
250
+ projectId: input.projectId,
251
+ complaint,
252
+ sessionCount: annotatedSessions.length,
253
+ sessionIds: annotatedSessions.map(session => session.sessionId),
254
+ eventCount: logs.normalized.eventCount,
255
+ logTimeRange: buildLogTimeRange(allEvents),
256
+ clockSkew: buildClockSkew(allEvents),
257
+ projectVersions: history.map(item => item.version),
258
+ suspiciousSignals: signals,
259
+ semanticsCoverage: buildSemanticsCoverage(annotatedSessions),
260
+ sourceRepos: sourceVersions.map(version => ({ id: version.repoId, reachable: version.reachable })),
261
+ missingEvidence,
262
+ logSource: {
263
+ source: logs.provenance.source,
264
+ ...(logs.provenance.file ? { file: logs.provenance.file } : {}),
265
+ ...(logSourceFilter ? { filter: logSourceFilter } : {}),
266
+ ...projectIdMatch,
267
+ },
268
+ };
269
+ writeArtifact(run.runDir, "brief.json", brief);
270
+ return { runId: run.runId, brief };
271
+ },
272
+ });
273
+ //# sourceMappingURL=create-run.js.map
@@ -0,0 +1,68 @@
1
+ import type { AnnotatedEvent, SessionGroup } from "@ats-cx/cx-core";
2
+ import type { CaseBrief } from "./create-run.js";
3
+ type AnnotatedSession = Omit<SessionGroup, "events"> & {
4
+ events: AnnotatedEvent[];
5
+ };
6
+ /**
7
+ * 时间线/检索的输出条目:只保留调查需要的字段 + 证据 ID,控制上下文体积。
8
+ * 默认只给 payloadKeys;`payload: true` 时原样附带 payload——没有这个口子,Agent 会绕过 CLI
9
+ * 直接 jq 读 artifacts 里的 sessions.annotated.json(真实 case 已出现),证据引用就断了。
10
+ */
11
+ export interface TimelineItem {
12
+ evidenceRef: string;
13
+ logTime: string;
14
+ eventName: string;
15
+ category: string;
16
+ stateChange: string;
17
+ payloadKeys: string[];
18
+ message: string | null;
19
+ payload?: Record<string, unknown>;
20
+ }
21
+ /** session 摘要:时间跨度 + 身份字段,够 Agent 分清「客户本人的会话」和「客服/研发复现的会话」。 */
22
+ export interface SessionSummary {
23
+ sessionId: string;
24
+ startedAt: string;
25
+ endedAt: string;
26
+ eventCount: number;
27
+ /** 去重后的 userId(空值剔除)。 */
28
+ userIds: string[];
29
+ /** 去重后的 `<deviceType>/<osType>`,如 `pc/Windows`。 */
30
+ devices: string[];
31
+ /** 去重后的路由(page_url 的 hash 路径),反映会话进了哪些产品页。 */
32
+ routes: string[];
33
+ }
34
+ type SessionLike = Pick<AnnotatedSession, "sessionId" | "startedAt" | "endedAt" | "eventCount"> & {
35
+ events: Array<Pick<AnnotatedEvent, "userId" | "device" | "route">>;
36
+ };
37
+ /**
38
+ * 按 startedAt 升序:sessions.annotated.json 的顺序来自日志源分组,与时间序无关,
39
+ * 真实 case 里客服 8/25 的会话排在客户会话前面,Agent 要自己重排才能看出先后。
40
+ */
41
+ export declare function summarizeSessions(sessions: SessionLike[]): SessionSummary[];
42
+ export declare const getCaseBriefTool: import("../contract.js").ToolDefinition<{
43
+ runId: string;
44
+ }, CaseBrief>;
45
+ export declare const listSessionsTool: import("../contract.js").ToolDefinition<{
46
+ runId: string;
47
+ }, {
48
+ sessions: SessionSummary[];
49
+ }>;
50
+ export declare const getTimelineTool: import("../contract.js").ToolDefinition<{
51
+ runId: string;
52
+ sessionId: string;
53
+ payload?: boolean | undefined;
54
+ category?: string | undefined;
55
+ cursor?: number | undefined;
56
+ pageSize?: number | undefined;
57
+ }, import("../budget.js").Page<TimelineItem>>;
58
+ export declare const searchLogsTool: import("../contract.js").ToolDefinition<{
59
+ runId: string;
60
+ eventName?: string | undefined;
61
+ payload?: boolean | undefined;
62
+ cursor?: number | undefined;
63
+ pageSize?: number | undefined;
64
+ payloadKey?: string | undefined;
65
+ from?: string | undefined;
66
+ to?: string | undefined;
67
+ }, import("../budget.js").Page<TimelineItem>>;
68
+ export {};
@@ -0,0 +1,116 @@
1
+ import { z } from "zod";
2
+ import { loadRun, readArtifact } from "@ats-cx/cx-core";
3
+ import { defineTool } from "../contract.js";
4
+ import { paginate } from "../budget.js";
5
+ function toTimelineItem(event, includePayload) {
6
+ const item = {
7
+ evidenceRef: `log:${event.sessionId}:${event.originalIndex}`,
8
+ logTime: event.logTime,
9
+ eventName: event.eventName,
10
+ category: event.semantics.category,
11
+ stateChange: event.semantics.stateChange,
12
+ payloadKeys: Object.keys(event.payload),
13
+ message: typeof event.payload.message === "string" ? event.payload.message : null,
14
+ };
15
+ if (includePayload) {
16
+ item.payload = event.payload;
17
+ }
18
+ return item;
19
+ }
20
+ function uniqueSorted(values) {
21
+ return [...new Set(values.filter(value => value.length > 0))].sort();
22
+ }
23
+ /**
24
+ * 按 startedAt 升序:sessions.annotated.json 的顺序来自日志源分组,与时间序无关,
25
+ * 真实 case 里客服 8/25 的会话排在客户会话前面,Agent 要自己重排才能看出先后。
26
+ */
27
+ export function summarizeSessions(sessions) {
28
+ return sessions
29
+ .map(session => ({
30
+ sessionId: session.sessionId,
31
+ startedAt: session.startedAt,
32
+ endedAt: session.endedAt,
33
+ eventCount: session.eventCount,
34
+ userIds: uniqueSorted(session.events.map(event => event.userId)),
35
+ devices: uniqueSorted(session.events.map(event => [event.device.deviceType, event.device.osType].filter(Boolean).join("/"))),
36
+ routes: uniqueSorted(session.events.map(event => event.route)),
37
+ }))
38
+ .sort((left, right) => left.startedAt.localeCompare(right.startedAt) || left.sessionId.localeCompare(right.sessionId));
39
+ }
40
+ function loadSessions(runsDir, runId) {
41
+ const run = loadRun(runsDir, runId);
42
+ return readArtifact(run.runDir, "sessions.annotated.json");
43
+ }
44
+ export const getCaseBriefTool = defineTool({
45
+ name: "get_case_brief",
46
+ description: "读取 run 的 case 摘要:可疑信号、可用证据清单、缺失证据",
47
+ inputSchema: z.object({ runId: z.string().min(1) }),
48
+ handler: async (input, context) => {
49
+ const run = loadRun(context.config.runsDir, input.runId);
50
+ return readArtifact(run.runDir, "brief.json");
51
+ },
52
+ });
53
+ export const listSessionsTool = defineTool({
54
+ name: "list_sessions",
55
+ description: "列出 run 内标准化后的日志 session 摘要",
56
+ inputSchema: z.object({ runId: z.string().min(1) }),
57
+ handler: async (input, context) => {
58
+ return { sessions: summarizeSessions(loadSessions(context.config.runsDir, input.runId)) };
59
+ },
60
+ });
61
+ export const getTimelineTool = defineTool({
62
+ name: "get_timeline",
63
+ description: "读取某 session 的事件时间线,支持分页与语义类别过滤",
64
+ inputSchema: z.object({
65
+ runId: z.string().min(1),
66
+ sessionId: z.string().min(1),
67
+ category: z.string().optional(),
68
+ payload: z.boolean().optional(),
69
+ cursor: z.number().int().min(0).optional(),
70
+ pageSize: z.number().int().min(1).max(200).optional(),
71
+ }),
72
+ handler: async (input, context) => {
73
+ const sessions = loadSessions(context.config.runsDir, input.runId);
74
+ const session = sessions.find(candidate => candidate.sessionId === input.sessionId);
75
+ if (!session) {
76
+ throw new Error(`session ${input.sessionId} 不存在,先用 list_sessions 查看可用 session`);
77
+ }
78
+ const filtered = input.category
79
+ ? session.events.filter(event => event.semantics.category === input.category)
80
+ : session.events;
81
+ const includePayload = input.payload === true;
82
+ return paginate(filtered.map(event => toTimelineItem(event, includePayload)), { cursor: input.cursor, pageSize: input.pageSize });
83
+ },
84
+ });
85
+ export const searchLogsTool = defineTool({
86
+ name: "search_logs",
87
+ description: "按事件名 / payload key / 时间范围跨 session 检索日志事件",
88
+ inputSchema: z.object({
89
+ runId: z.string().min(1),
90
+ eventName: z.string().optional(),
91
+ payloadKey: z.string().optional(),
92
+ from: z.string().optional(),
93
+ to: z.string().optional(),
94
+ payload: z.boolean().optional(),
95
+ cursor: z.number().int().min(0).optional(),
96
+ pageSize: z.number().int().min(1).max(200).optional(),
97
+ }),
98
+ handler: async (input, context) => {
99
+ const sessions = loadSessions(context.config.runsDir, input.runId);
100
+ const all = sessions.flatMap(session => session.events);
101
+ const filtered = all.filter(event => {
102
+ if (input.eventName && event.eventName !== input.eventName)
103
+ return false;
104
+ if (input.payloadKey && !(input.payloadKey in event.payload))
105
+ return false;
106
+ if (input.from && event.logTimestamp < Date.parse(input.from))
107
+ return false;
108
+ if (input.to && event.logTimestamp > Date.parse(input.to))
109
+ return false;
110
+ return true;
111
+ });
112
+ const includePayload = input.payload === true;
113
+ return paginate(filtered.map(event => toTimelineItem(event, includePayload)), { cursor: input.cursor, pageSize: input.pageSize });
114
+ },
115
+ });
116
+ //# sourceMappingURL=log-tools.js.map
@@ -0,0 +1,82 @@
1
+ import type { NormalizedProject, ProjectDiffResult } from "@ats-cx/cx-core";
2
+ import { paginate } from "../budget.js";
3
+ declare function buildSnapshotSummary(project: NormalizedProject): {
4
+ projectMeta: Record<string, unknown>;
5
+ pageCount: number;
6
+ elementCount: number;
7
+ pageCollectionCounts: Record<string, number>;
8
+ elementTypeCounts: Record<string, number>;
9
+ };
10
+ type PageSectionDiffs = ProjectDiffResult["pageSectionDiffs"];
11
+ /**
12
+ * 页级属性变化(如 pages[n].type)只存在于 pageSectionDiffs,
13
+ * 不会出现在元素级 changedElements 里。stdout 若丢弃该结构,
14
+ * “只有页面 type 变了”的 diff 会被误读成“没有变化”(客诉 23862 案例)。
15
+ * 这里只透传 summary + changedItems(限量);各区域的增删明细
16
+ * 已由顶层 addedPages / removedPages 承载,不重复输出。
17
+ */
18
+ declare function shapeSectionDiff(section: PageSectionDiffs["pages"]): {
19
+ key: string;
20
+ label: string;
21
+ beforeCount: number;
22
+ afterCount: number;
23
+ summary: {
24
+ addedItemCount: number;
25
+ removedItemCount: number;
26
+ changedItemCount: number;
27
+ };
28
+ changedItems: import("@ats-cx/cx-core/dist/diff/project-diff.js").ChangedSectionItemDiff[];
29
+ };
30
+ declare function shapeCoverSectionDiff(section: PageSectionDiffs["cover"]): {
31
+ key: string;
32
+ label: string;
33
+ beforeCount: number;
34
+ afterCount: number;
35
+ summary: {
36
+ changedFieldCount: number;
37
+ addedItemCount: number;
38
+ removedItemCount: number;
39
+ changedItemCount: number;
40
+ };
41
+ changedFields: import("@ats-cx/cx-core/dist/diff/project-diff.js").FieldChange[];
42
+ changedItems: import("@ats-cx/cx-core/dist/diff/project-diff.js").ChangedSectionItemDiff[];
43
+ };
44
+ export declare const getProjectSnapshotTool: import("../contract.js").ToolDefinition<{
45
+ runId: string;
46
+ version: string;
47
+ cursor?: number | undefined;
48
+ pageSize?: number | undefined;
49
+ detail?: "pages" | "summary" | "elements" | undefined;
50
+ }, {
51
+ version: string;
52
+ evidenceRef: string;
53
+ summary: ReturnType<typeof buildSnapshotSummary>;
54
+ pages?: ReturnType<typeof paginate<NormalizedProject["pages"][number]>>;
55
+ elements?: ReturnType<typeof paginate<NormalizedProject["elements"][number]>>;
56
+ }>;
57
+ export declare const diffProjectVersionsTool: import("../contract.js").ToolDefinition<{
58
+ runId: string;
59
+ before: string;
60
+ after: string;
61
+ cursor?: number | undefined;
62
+ pageSize?: number | undefined;
63
+ }, {
64
+ evidenceRef: string;
65
+ summary: ProjectDiffResult["summary"];
66
+ pageSectionDiffs: {
67
+ pages: ReturnType<typeof shapeSectionDiff>;
68
+ cover: ReturnType<typeof shapeCoverSectionDiff>;
69
+ frontFlysheet: ReturnType<typeof shapeSectionDiff>;
70
+ backFlysheet: ReturnType<typeof shapeSectionDiff>;
71
+ edge: ReturnType<typeof shapeSectionDiff>;
72
+ };
73
+ changedProjectMeta: ProjectDiffResult["changedProjectMeta"];
74
+ addedPages: ProjectDiffResult["addedPages"];
75
+ removedPages: ProjectDiffResult["removedPages"];
76
+ changedElements: ProjectDiffResult["changedElements"];
77
+ addedElements: ProjectDiffResult["addedElements"];
78
+ removedElements: ProjectDiffResult["removedElements"];
79
+ truncated: boolean;
80
+ hint: string | null;
81
+ }>;
82
+ export {};
@@ -0,0 +1,139 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { z } from "zod";
4
+ import { diffProjectSnapshots, loadRun, normalizeProjectSnapshot, readArtifact, writeArtifact, } from "@ats-cx/cx-core";
5
+ import { defineTool } from "../contract.js";
6
+ import { paginate } from "../budget.js";
7
+ const MAX_LIST_ITEMS = 50;
8
+ function loadHistoryIndex(runDir) {
9
+ return readArtifact(runDir, "project-history.index.json");
10
+ }
11
+ function findVersionEntry(index, version) {
12
+ const entry = index.find(item => item.version === version);
13
+ if (!entry) {
14
+ const available = index.map(item => item.version).join(", ") || "(无)";
15
+ throw new Error(`project 版本 ${version} 不存在,可用版本:${available}`);
16
+ }
17
+ return entry;
18
+ }
19
+ function loadRawSnapshot(entry) {
20
+ return JSON.parse(readFileSync(entry.filePath, "utf8"));
21
+ }
22
+ function buildSnapshotSummary(project) {
23
+ return {
24
+ projectMeta: project.projectMeta,
25
+ pageCount: project.pageCount,
26
+ elementCount: project.elementCount,
27
+ pageCollectionCounts: project.pageCollectionCounts,
28
+ elementTypeCounts: project.elementTypeCounts,
29
+ };
30
+ }
31
+ function sliceList(items) {
32
+ return items.slice(0, MAX_LIST_ITEMS);
33
+ }
34
+ /**
35
+ * 页级属性变化(如 pages[n].type)只存在于 pageSectionDiffs,
36
+ * 不会出现在元素级 changedElements 里。stdout 若丢弃该结构,
37
+ * “只有页面 type 变了”的 diff 会被误读成“没有变化”(客诉 23862 案例)。
38
+ * 这里只透传 summary + changedItems(限量);各区域的增删明细
39
+ * 已由顶层 addedPages / removedPages 承载,不重复输出。
40
+ */
41
+ function shapeSectionDiff(section) {
42
+ return {
43
+ key: section.key,
44
+ label: section.label,
45
+ beforeCount: section.beforeCount,
46
+ afterCount: section.afterCount,
47
+ summary: section.summary,
48
+ changedItems: sliceList(section.changedItems),
49
+ };
50
+ }
51
+ function shapeCoverSectionDiff(section) {
52
+ return {
53
+ key: section.key,
54
+ label: section.label,
55
+ beforeCount: section.beforeCount,
56
+ afterCount: section.afterCount,
57
+ summary: section.summary,
58
+ changedFields: section.changedFields,
59
+ changedItems: sliceList(section.changedItems),
60
+ };
61
+ }
62
+ export const getProjectSnapshotTool = defineTool({
63
+ name: "get_project_snapshot",
64
+ description: "读取指定版本的 project 快照摘要,可选分页返回页面或元素明细",
65
+ inputSchema: z.object({
66
+ runId: z.string().min(1),
67
+ version: z.string().min(1),
68
+ detail: z.enum(["summary", "pages", "elements"]).optional(),
69
+ cursor: z.number().int().min(0).optional(),
70
+ pageSize: z.number().int().min(1).max(200).optional(),
71
+ }),
72
+ handler: async (input, context) => {
73
+ const run = loadRun(context.config.runsDir, input.runId);
74
+ const index = loadHistoryIndex(run.runDir);
75
+ const entry = findVersionEntry(index, input.version);
76
+ const project = normalizeProjectSnapshot(loadRawSnapshot(entry), entry.filePath);
77
+ const detail = input.detail ?? "summary";
78
+ const output = {
79
+ version: input.version,
80
+ evidenceRef: `snapshot:${input.version}`,
81
+ summary: buildSnapshotSummary(project),
82
+ };
83
+ if (detail === "pages") {
84
+ output.pages = paginate(project.pages, { cursor: input.cursor, pageSize: input.pageSize });
85
+ }
86
+ if (detail === "elements") {
87
+ output.elements = paginate(project.elements, { cursor: input.cursor, pageSize: input.pageSize });
88
+ }
89
+ return output;
90
+ },
91
+ });
92
+ export const diffProjectVersionsTool = defineTool({
93
+ name: "diff_project_versions",
94
+ description: "对比两个 project 版本,优先读取预计算 diff,支持 changedElements 分页",
95
+ inputSchema: z.object({
96
+ runId: z.string().min(1),
97
+ before: z.string().min(1),
98
+ after: z.string().min(1),
99
+ cursor: z.number().int().min(0).optional(),
100
+ pageSize: z.number().int().min(1).max(200).optional(),
101
+ }),
102
+ handler: async (input, context) => {
103
+ const run = loadRun(context.config.runsDir, input.runId);
104
+ const artifactName = `project-diff.${input.before}-${input.after}.json`;
105
+ const artifactPath = join(run.runDir, "artifacts", artifactName);
106
+ let diff;
107
+ if (existsSync(artifactPath)) {
108
+ diff = readArtifact(run.runDir, artifactName);
109
+ }
110
+ else {
111
+ const index = loadHistoryIndex(run.runDir);
112
+ const beforeEntry = findVersionEntry(index, input.before);
113
+ const afterEntry = findVersionEntry(index, input.after);
114
+ diff = diffProjectSnapshots(loadRawSnapshot(beforeEntry), loadRawSnapshot(afterEntry));
115
+ writeArtifact(run.runDir, artifactName, diff);
116
+ }
117
+ const changedPage = paginate(diff.changedElements, { cursor: input.cursor, pageSize: input.pageSize });
118
+ return {
119
+ evidenceRef: `diff:${input.before}-${input.after}`,
120
+ summary: diff.summary,
121
+ pageSectionDiffs: {
122
+ pages: shapeSectionDiff(diff.pageSectionDiffs.pages),
123
+ cover: shapeCoverSectionDiff(diff.pageSectionDiffs.cover),
124
+ frontFlysheet: shapeSectionDiff(diff.pageSectionDiffs.frontFlysheet),
125
+ backFlysheet: shapeSectionDiff(diff.pageSectionDiffs.backFlysheet),
126
+ edge: shapeSectionDiff(diff.pageSectionDiffs.edge),
127
+ },
128
+ changedProjectMeta: diff.changedProjectMeta,
129
+ addedPages: sliceList(diff.addedPages),
130
+ removedPages: sliceList(diff.removedPages),
131
+ changedElements: changedPage.items,
132
+ addedElements: sliceList(diff.addedElements),
133
+ removedElements: sliceList(diff.removedElements),
134
+ truncated: changedPage.truncated,
135
+ hint: changedPage.hint,
136
+ };
137
+ },
138
+ });
139
+ //# sourceMappingURL=project-tools.js.map