@ai-setting/roy-agent-cli 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 +126 -0
- package/dist/bin/roy.js +127297 -0
- package/dist/roy-agent-darwin-arm64/bin/roy.js +127297 -0
- package/dist/roy-agent-darwin-x64/bin/roy.js +127297 -0
- package/dist/roy-agent-linux-arm64/bin/roy.js +127297 -0
- package/dist/roy-agent-linux-x64/bin/roy.js +127297 -0
- package/dist/roy-agent-windows-x64/bin/roy.js +127297 -0
- package/package.json +91 -0
- package/src/bin/roy.ts +12 -0
- package/src/cli.ts +101 -0
- package/src/commands/act.ts +480 -0
- package/src/commands/commands-add.ts +110 -0
- package/src/commands/commands-dirs.ts +70 -0
- package/src/commands/commands-info.ts +90 -0
- package/src/commands/commands-list.ts +161 -0
- package/src/commands/commands-remove.ts +147 -0
- package/src/commands/commands.ts +55 -0
- package/src/commands/config/config-service.test.ts +449 -0
- package/src/commands/config/config-service.ts +312 -0
- package/src/commands/config/deep-merge.test.ts +168 -0
- package/src/commands/config/deep-merge.ts +63 -0
- package/src/commands/config/export.ts +97 -0
- package/src/commands/config/filter-history-e2e.test.ts +141 -0
- package/src/commands/config/import-preserve-refs.test.ts +212 -0
- package/src/commands/config/import.ts +119 -0
- package/src/commands/config/index.ts +35 -0
- package/src/commands/config/list.ts +281 -0
- package/src/commands/config/roy-config-e2e.test.ts +297 -0
- package/src/commands/config/types.ts +54 -0
- package/src/commands/debug/index.ts +38 -0
- package/src/commands/debug/log.test.ts +233 -0
- package/src/commands/debug/log.ts +123 -0
- package/src/commands/debug/span.test.ts +297 -0
- package/src/commands/debug/span.ts +211 -0
- package/src/commands/debug/trace.test.ts +254 -0
- package/src/commands/debug/trace.ts +140 -0
- package/src/commands/eventsource/add.ts +133 -0
- package/src/commands/eventsource/index.ts +48 -0
- package/src/commands/eventsource/list.ts +194 -0
- package/src/commands/eventsource/remove.ts +95 -0
- package/src/commands/eventsource/start.ts +103 -0
- package/src/commands/eventsource/status.ts +185 -0
- package/src/commands/eventsource/stop.ts +89 -0
- package/src/commands/index.ts +22 -0
- package/src/commands/input-handler.test.ts +76 -0
- package/src/commands/input-handler.ts +43 -0
- package/src/commands/interactive-esc.test.ts +254 -0
- package/src/commands/interactive.shutdown.test.ts +122 -0
- package/src/commands/interactive.test.ts +221 -0
- package/src/commands/interactive.ts +1015 -0
- package/src/commands/lsp/check.ts +92 -0
- package/src/commands/lsp/index.ts +32 -0
- package/src/commands/lsp/install.ts +126 -0
- package/src/commands/lsp/list.ts +64 -0
- package/src/commands/mcp/index.ts +27 -0
- package/src/commands/mcp/list.ts +116 -0
- package/src/commands/mcp/reload.ts +70 -0
- package/src/commands/mcp/tools.ts +121 -0
- package/src/commands/memory/extract-e2e.test.ts +388 -0
- package/src/commands/memory/index.ts +11 -0
- package/src/commands/memory/memory-simplified.test.ts +58 -0
- package/src/commands/memory/memory.ts +25 -0
- package/src/commands/memory/organize.ts +300 -0
- package/src/commands/memory/recall.test.ts +120 -0
- package/src/commands/memory/recall.ts +88 -0
- package/src/commands/memory/record-extract-handle-query.test.ts +385 -0
- package/src/commands/memory/record-prompt-component.test.ts +343 -0
- package/src/commands/memory/record.test.ts +92 -0
- package/src/commands/memory/record.ts +332 -0
- package/src/commands/plugin.test.ts +292 -0
- package/src/commands/plugin.ts +267 -0
- package/src/commands/sessions/active.ts +96 -0
- package/src/commands/sessions/add-message.ts +96 -0
- package/src/commands/sessions/checkpoints.ts +154 -0
- package/src/commands/sessions/compact.test.ts +215 -0
- package/src/commands/sessions/compact.ts +269 -0
- package/src/commands/sessions/delete.ts +236 -0
- package/src/commands/sessions/get.ts +165 -0
- package/src/commands/sessions/grep.ts +233 -0
- package/src/commands/sessions/index.ts +95 -0
- package/src/commands/sessions/list.ts +210 -0
- package/src/commands/sessions/messages.test.ts +333 -0
- package/src/commands/sessions/messages.ts +248 -0
- package/src/commands/sessions/mock.ts +194 -0
- package/src/commands/sessions/new.ts +82 -0
- package/src/commands/sessions/rename.ts +98 -0
- package/src/commands/shared/event-handler.ts +213 -0
- package/src/commands/shared/event-message-formatter.ts +295 -0
- package/src/commands/shared/index.ts +11 -0
- package/src/commands/shared/query-executor.test.ts +434 -0
- package/src/commands/shared/query-executor.ts +324 -0
- package/src/commands/shared/repl-engine.test.ts +354 -0
- package/src/commands/shared/session-manager.test.ts +212 -0
- package/src/commands/shared/session-manager.ts +114 -0
- package/src/commands/skills/get.ts +90 -0
- package/src/commands/skills/index.ts +39 -0
- package/src/commands/skills/list.ts +129 -0
- package/src/commands/skills/reload.ts +59 -0
- package/src/commands/skills/search.ts +132 -0
- package/src/commands/skills/show-config.ts +93 -0
- package/src/commands/tasks/complete.ts +92 -0
- package/src/commands/tasks/create.ts +118 -0
- package/src/commands/tasks/delete.ts +86 -0
- package/src/commands/tasks/get.ts +116 -0
- package/src/commands/tasks/index.ts +53 -0
- package/src/commands/tasks/list.ts +140 -0
- package/src/commands/tasks/operations.ts +120 -0
- package/src/commands/tasks/update.ts +122 -0
- package/src/commands/tools/exec-tool.ts +128 -0
- package/src/commands/tools/get.ts +114 -0
- package/src/commands/tools/index.ts +35 -0
- package/src/commands/tools/list.ts +107 -0
- package/src/commands/tools/shared/index.ts +7 -0
- package/src/commands/tools/shared/schema-helper.ts +111 -0
- package/src/commands/workflow/commands/add.ts +315 -0
- package/src/commands/workflow/commands/get.ts +193 -0
- package/src/commands/workflow/commands/list.ts +137 -0
- package/src/commands/workflow/commands/nodes.ts +528 -0
- package/src/commands/workflow/commands/remove.ts +94 -0
- package/src/commands/workflow/commands/run.ts +398 -0
- package/src/commands/workflow/commands/status.ts +147 -0
- package/src/commands/workflow/commands/stop.ts +91 -0
- package/src/commands/workflow/commands/update.ts +130 -0
- package/src/commands/workflow/commands/validate.ts +139 -0
- package/src/commands/workflow/commands/workflow-cli.test.ts +196 -0
- package/src/commands/workflow/index.ts +65 -0
- package/src/commands/workflow/renderers.ts +358 -0
- package/src/commands/workflow/validators/index.ts +8 -0
- package/src/commands/workflow/validators/node-validator-factory.ts +40 -0
- package/src/commands/workflow/validators/node-validator.ts +125 -0
- package/src/commands/workflow/validators/nodes/agent-node-validator.ts +58 -0
- package/src/commands/workflow/validators/nodes/condition-node-validator.ts +34 -0
- package/src/commands/workflow/validators/nodes/decorator-node-validator.ts +45 -0
- package/src/commands/workflow/validators/nodes/merge-node-validator.ts +46 -0
- package/src/commands/workflow/validators/nodes/skill-node-validator.ts +33 -0
- package/src/commands/workflow/validators/nodes/tool-node-validator.ts +54 -0
- package/src/commands/workflow/validators/nodes/workflow-node-validator.ts +33 -0
- package/src/commands/workflow/validators/types.ts +78 -0
- package/src/commands/workflow/validators/workflow-validator.test.ts +273 -0
- package/src/commands/workflow/validators/workflow-validator.ts +320 -0
- package/src/index.ts +19 -0
- package/src/plugin/apply.ts +103 -0
- package/src/plugin/discover.ts +219 -0
- package/src/plugin/index.ts +45 -0
- package/src/plugin/registry.ts +272 -0
- package/src/plugin/types.ts +165 -0
- package/src/services/context-handler.service.test.ts +501 -0
- package/src/services/context-handler.service.ts +372 -0
- package/src/services/environment.service.commands-prompt.test.ts +167 -0
- package/src/services/environment.service.ts +656 -0
- package/src/services/output.service.test.ts +92 -0
- package/src/services/output.service.ts +122 -0
- package/src/services/quiet-mode.service.test.ts +114 -0
- package/src/services/quiet-mode.service.ts +81 -0
- package/src/services/stream-output.service.test.ts +214 -0
- package/src/services/stream-output.service.ts +323 -0
- package/src/util/which.test.ts +101 -0
- package/src/util/which.ts +55 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Debug Span Command
|
|
3
|
+
*
|
|
4
|
+
* Command: roy debug span <spanId>
|
|
5
|
+
*
|
|
6
|
+
* 通过 spanId 从 SQLite 中查询详细的 span 信息
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as path from "path";
|
|
10
|
+
import * as os from "os";
|
|
11
|
+
import type { CommandModule } from "yargs";
|
|
12
|
+
import { SQLiteSpanStorage } from "@ai-setting/roy-agent-core";
|
|
13
|
+
import type { Span } from "@ai-setting/roy-agent-core";
|
|
14
|
+
|
|
15
|
+
interface SpanOptions {
|
|
16
|
+
spanId?: string;
|
|
17
|
+
db?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const SpanCommand: CommandModule<object, SpanOptions> = {
|
|
21
|
+
command: "span",
|
|
22
|
+
describe: "通过 spanId 查询详细的 span 信息",
|
|
23
|
+
|
|
24
|
+
builder: (yargs) =>
|
|
25
|
+
yargs
|
|
26
|
+
.positional("spanId", {
|
|
27
|
+
type: "string",
|
|
28
|
+
describe: "Span ID",
|
|
29
|
+
demandOption: true,
|
|
30
|
+
})
|
|
31
|
+
.option("db", {
|
|
32
|
+
type: "string",
|
|
33
|
+
describe: "SQLite 数据库路径(可选,默认使用配置路径)",
|
|
34
|
+
}),
|
|
35
|
+
|
|
36
|
+
async handler(args) {
|
|
37
|
+
const spanId = args._[2] as string || args.spanId;
|
|
38
|
+
|
|
39
|
+
if (!spanId) {
|
|
40
|
+
console.error("Error: spanId is required");
|
|
41
|
+
console.error("Usage: roy debug span <spanId>");
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 获取数据库路径
|
|
46
|
+
const dbPath = args.db || getDefaultSpanDbPath();
|
|
47
|
+
|
|
48
|
+
// 创建存储实例并读取数据
|
|
49
|
+
const storage = new SQLiteSpanStorage(dbPath);
|
|
50
|
+
await storage.initialize();
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
// 查询 span
|
|
54
|
+
const span = storage.findBySpanId(spanId);
|
|
55
|
+
|
|
56
|
+
if (!span) {
|
|
57
|
+
console.log(`Span ${spanId} not found.`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 格式化输出详细信息
|
|
62
|
+
const output = formatSpanDetail(span);
|
|
63
|
+
console.log(output);
|
|
64
|
+
} finally {
|
|
65
|
+
storage.close();
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 获取默认的 span 数据库路径
|
|
72
|
+
*/
|
|
73
|
+
export function getDefaultSpanDbPath(): string {
|
|
74
|
+
// 支持环境变量覆盖
|
|
75
|
+
if (process.env.LOG_TRACE_TRACING_DB_PATH) {
|
|
76
|
+
return process.env.LOG_TRACE_TRACING_DB_PATH;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 使用 XDG 标准路径
|
|
80
|
+
const home = os.homedir();
|
|
81
|
+
const dataHome = process.env.XDG_DATA_HOME || path.join(home, ".local", "share");
|
|
82
|
+
return path.join(dataHome, "roy-agent", "traces.db");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 安全序列化对象,处理循环引用和其他异常
|
|
87
|
+
*/
|
|
88
|
+
function safeStringify(obj: unknown, indent: number = 2): string {
|
|
89
|
+
// 处理非对象类型
|
|
90
|
+
if (obj === null) return "null";
|
|
91
|
+
if (obj === undefined) return "undefined";
|
|
92
|
+
if (typeof obj !== "object") return String(obj);
|
|
93
|
+
|
|
94
|
+
// 如果已经是字符串,可能是已经 stringify 过的
|
|
95
|
+
if (typeof obj === "string") {
|
|
96
|
+
// 尝试检测是否是 JSON 字符串
|
|
97
|
+
try {
|
|
98
|
+
const parsed = JSON.parse(obj);
|
|
99
|
+
return safeStringify(parsed, indent);
|
|
100
|
+
} catch {
|
|
101
|
+
return obj;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 处理数组
|
|
106
|
+
if (Array.isArray(obj)) {
|
|
107
|
+
try {
|
|
108
|
+
const items = obj.map(item => safeStringify(item, 0));
|
|
109
|
+
return `[${items.join(", ")}]`;
|
|
110
|
+
} catch {
|
|
111
|
+
return "[Array - cannot stringify]";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 使用 WeakSet 检测循环引用
|
|
116
|
+
const seen = new WeakSet<object>();
|
|
117
|
+
|
|
118
|
+
const stringify = (value: unknown, depth: number): string => {
|
|
119
|
+
if (value === null) return "null";
|
|
120
|
+
if (value === undefined) return "undefined";
|
|
121
|
+
if (typeof value !== "object") return JSON.stringify(value);
|
|
122
|
+
|
|
123
|
+
if (typeof value === "string") {
|
|
124
|
+
// 可能是已经 stringify 过的 JSON 字符串
|
|
125
|
+
try {
|
|
126
|
+
const parsed = JSON.parse(value);
|
|
127
|
+
return JSON.stringify(parsed);
|
|
128
|
+
} catch {
|
|
129
|
+
return JSON.stringify(value);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (Array.isArray(value)) {
|
|
134
|
+
if (seen.has(value)) return '"[Circular]"';
|
|
135
|
+
seen.add(value);
|
|
136
|
+
const items = value.map(item => stringify(item, depth + 1));
|
|
137
|
+
return `[${items.join(", ")}]`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (seen.has(value)) return '"[Circular]"';
|
|
141
|
+
seen.add(value);
|
|
142
|
+
|
|
143
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
144
|
+
const props = entries.map(([k, v]) => `"${k}": ${stringify(v, depth + 1)}`);
|
|
145
|
+
return `{${props.join(", ")}}`;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
return stringify(obj, 0);
|
|
150
|
+
} catch (e) {
|
|
151
|
+
return `[Object - cannot stringify: ${e instanceof Error ? e.message : String(e)}]`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 格式化 span 为详细输出
|
|
157
|
+
*/
|
|
158
|
+
export function formatSpanDetail(span: Span): string {
|
|
159
|
+
const lines: string[] = [];
|
|
160
|
+
|
|
161
|
+
// 基本信息
|
|
162
|
+
lines.push(`Span ID: ${span.spanId}`);
|
|
163
|
+
lines.push(`Trace ID: ${span.traceId}`);
|
|
164
|
+
lines.push(`Name: ${span.name}`);
|
|
165
|
+
lines.push(`Kind: ${span.kind}`);
|
|
166
|
+
lines.push(`Status: ${span.status}`);
|
|
167
|
+
|
|
168
|
+
// 时间信息
|
|
169
|
+
const duration = span.endTime
|
|
170
|
+
? `${span.endTime - span.startTime}ms`
|
|
171
|
+
: "(running)";
|
|
172
|
+
lines.push(`Duration: ${duration}`);
|
|
173
|
+
lines.push(`Start: ${span.startTime} (${new Date(span.startTime).toISOString()})`);
|
|
174
|
+
if (span.endTime) {
|
|
175
|
+
lines.push(`End: ${span.endTime} (${new Date(span.endTime).toISOString()})`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 父子关系
|
|
179
|
+
if (span.parentSpanId) {
|
|
180
|
+
lines.push(`Parent: ${span.parentSpanId}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 错误信息
|
|
184
|
+
if (span.error) {
|
|
185
|
+
lines.push(`Error: ${span.error}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 属性
|
|
189
|
+
if (span.attributes && Object.keys(span.attributes).length > 0) {
|
|
190
|
+
lines.push("");
|
|
191
|
+
lines.push("Attributes:");
|
|
192
|
+
for (const [key, value] of Object.entries(span.attributes)) {
|
|
193
|
+
lines.push(` ${key}: ${safeStringify(value, 0)}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 结果
|
|
198
|
+
if (span.result !== undefined) {
|
|
199
|
+
lines.push("");
|
|
200
|
+
lines.push("Result:");
|
|
201
|
+
// 如果是字符串,尝试解析为 JSON 显示;如果是已损坏的数据,显示提示
|
|
202
|
+
const resultStr = typeof span.result === "string"
|
|
203
|
+
? (span.result.includes("[Object with circular reference")
|
|
204
|
+
? "[Circular reference - original data contained circular structures]"
|
|
205
|
+
: span.result)
|
|
206
|
+
: safeStringify(span.result);
|
|
207
|
+
lines.push(` ${resultStr}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return lines.join("\n");
|
|
211
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Debug Trace Command - TDD Tests
|
|
3
|
+
*
|
|
4
|
+
* TDD 开发流程:
|
|
5
|
+
* 1. RED - 编写失败的测试
|
|
6
|
+
* 2. GREEN - 编写最小代码让测试通过
|
|
7
|
+
* 3. REFACTOR - 重构
|
|
8
|
+
*
|
|
9
|
+
* 测试目标: debug trace <traceId>
|
|
10
|
+
* 功能: 以 tree 方式展示 traceId 的 spans
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
|
|
14
|
+
import type { Span } from "@ai-setting/roy-agent-core";
|
|
15
|
+
import { SpanKind, SpanStatus } from "@ai-setting/roy-agent-core";
|
|
16
|
+
|
|
17
|
+
describe("Debug Trace Command - TDD", () => {
|
|
18
|
+
describe("RED: getDefaultSpanDbPath()", () => {
|
|
19
|
+
test("should return path in XDG_DATA_HOME/roy-agent/traces.db by default", async () => {
|
|
20
|
+
const { getDefaultSpanDbPath } = await import("./trace");
|
|
21
|
+
|
|
22
|
+
// Clear environment variables
|
|
23
|
+
delete process.env.LOG_TRACE_TRACING_DB_PATH;
|
|
24
|
+
|
|
25
|
+
const dbPath = getDefaultSpanDbPath();
|
|
26
|
+
|
|
27
|
+
expect(dbPath).toContain("roy-agent");
|
|
28
|
+
expect(dbPath).toContain("traces.db");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("should use LOG_TRACE_TRACING_DB_PATH environment variable when set", async () => {
|
|
32
|
+
const { getDefaultSpanDbPath } = await import("./trace");
|
|
33
|
+
|
|
34
|
+
const customPath = "/custom/path/my-traces.db";
|
|
35
|
+
process.env.LOG_TRACE_TRACING_DB_PATH = customPath;
|
|
36
|
+
|
|
37
|
+
const dbPath = getDefaultSpanDbPath();
|
|
38
|
+
|
|
39
|
+
expect(dbPath).toBe(customPath);
|
|
40
|
+
|
|
41
|
+
delete process.env.LOG_TRACE_TRACING_DB_PATH;
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("RED: formatSpanTree()", () => {
|
|
46
|
+
test("should format single root span", async () => {
|
|
47
|
+
const { formatSpanTree } = await import("./trace");
|
|
48
|
+
|
|
49
|
+
const spans: Span[] = [{
|
|
50
|
+
traceId: "trace_1",
|
|
51
|
+
spanId: "span_1",
|
|
52
|
+
name: "root",
|
|
53
|
+
kind: SpanKind.INTERNAL,
|
|
54
|
+
status: SpanStatus.OK,
|
|
55
|
+
startTime: 1000,
|
|
56
|
+
endTime: 2000,
|
|
57
|
+
attributes: {},
|
|
58
|
+
children: [],
|
|
59
|
+
}];
|
|
60
|
+
|
|
61
|
+
const output = formatSpanTree(spans);
|
|
62
|
+
|
|
63
|
+
expect(output).toContain("root");
|
|
64
|
+
expect(output).toContain("1000ms"); // duration
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("should format span with children showing indentation", async () => {
|
|
68
|
+
const { formatSpanTree } = await import("./trace");
|
|
69
|
+
|
|
70
|
+
// spans 是已经构建好的树结构(根节点数组,每个节点有 children)
|
|
71
|
+
const spans: Span[] = [
|
|
72
|
+
{
|
|
73
|
+
traceId: "trace_1",
|
|
74
|
+
spanId: "span_1",
|
|
75
|
+
name: "root",
|
|
76
|
+
kind: SpanKind.INTERNAL,
|
|
77
|
+
status: SpanStatus.OK,
|
|
78
|
+
startTime: 1000,
|
|
79
|
+
endTime: 2000,
|
|
80
|
+
attributes: {},
|
|
81
|
+
children: [
|
|
82
|
+
{
|
|
83
|
+
traceId: "trace_1",
|
|
84
|
+
spanId: "span_2",
|
|
85
|
+
parentSpanId: "span_1",
|
|
86
|
+
name: "child",
|
|
87
|
+
kind: SpanKind.INTERNAL,
|
|
88
|
+
status: SpanStatus.OK,
|
|
89
|
+
startTime: 1200,
|
|
90
|
+
endTime: 1500,
|
|
91
|
+
attributes: {},
|
|
92
|
+
children: [],
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
const output = formatSpanTree(spans);
|
|
99
|
+
|
|
100
|
+
expect(output).toContain("root");
|
|
101
|
+
expect(output).toContain("child");
|
|
102
|
+
// Should show child under parent with indentation indicator
|
|
103
|
+
expect(output).toContain("└──") || expect(output).toContain("child");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("should calculate duration correctly", async () => {
|
|
107
|
+
const { formatSpanTree } = await import("./trace");
|
|
108
|
+
|
|
109
|
+
const spans: Span[] = [{
|
|
110
|
+
traceId: "trace_1",
|
|
111
|
+
spanId: "span_1",
|
|
112
|
+
name: "operation",
|
|
113
|
+
kind: SpanKind.INTERNAL,
|
|
114
|
+
status: SpanStatus.OK,
|
|
115
|
+
startTime: 1000,
|
|
116
|
+
endTime: 2500, // 1500ms duration
|
|
117
|
+
attributes: {},
|
|
118
|
+
children: [],
|
|
119
|
+
}];
|
|
120
|
+
|
|
121
|
+
const output = formatSpanTree(spans);
|
|
122
|
+
|
|
123
|
+
expect(output).toContain("1500ms");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("should handle span without endTime (running)", async () => {
|
|
127
|
+
const { formatSpanTree } = await import("./trace");
|
|
128
|
+
|
|
129
|
+
const spans: Span[] = [{
|
|
130
|
+
traceId: "trace_1",
|
|
131
|
+
spanId: "span_1",
|
|
132
|
+
name: "running-operation",
|
|
133
|
+
kind: SpanKind.INTERNAL,
|
|
134
|
+
status: SpanStatus.OK,
|
|
135
|
+
startTime: 1000,
|
|
136
|
+
endTime: undefined,
|
|
137
|
+
attributes: {},
|
|
138
|
+
children: [],
|
|
139
|
+
}];
|
|
140
|
+
|
|
141
|
+
const output = formatSpanTree(spans);
|
|
142
|
+
|
|
143
|
+
expect(output).toContain("running-operation");
|
|
144
|
+
expect(output).toContain("(running)");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("should show error indicator for failed spans", async () => {
|
|
148
|
+
const { formatSpanTree } = await import("./trace");
|
|
149
|
+
|
|
150
|
+
const spans: Span[] = [{
|
|
151
|
+
traceId: "trace_1",
|
|
152
|
+
spanId: "span_1",
|
|
153
|
+
name: "failed-operation",
|
|
154
|
+
kind: SpanKind.INTERNAL,
|
|
155
|
+
status: SpanStatus.ERROR,
|
|
156
|
+
startTime: 1000,
|
|
157
|
+
endTime: 2000,
|
|
158
|
+
attributes: {},
|
|
159
|
+
error: "Something went wrong",
|
|
160
|
+
children: [],
|
|
161
|
+
}];
|
|
162
|
+
|
|
163
|
+
const output = formatSpanTree(spans);
|
|
164
|
+
|
|
165
|
+
expect(output).toContain("failed-operation");
|
|
166
|
+
expect(output).toContain("Something went wrong");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("should handle empty spans array", async () => {
|
|
170
|
+
const { formatSpanTree } = await import("./trace");
|
|
171
|
+
|
|
172
|
+
const output = formatSpanTree([]);
|
|
173
|
+
|
|
174
|
+
expect(output).toContain("(empty)");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("should show spanId in output", async () => {
|
|
178
|
+
const { formatSpanTree } = await import("./trace");
|
|
179
|
+
|
|
180
|
+
const spans: Span[] = [{
|
|
181
|
+
traceId: "trace_1",
|
|
182
|
+
spanId: "span_1",
|
|
183
|
+
name: "operation",
|
|
184
|
+
kind: SpanKind.INTERNAL,
|
|
185
|
+
status: SpanStatus.OK,
|
|
186
|
+
startTime: 1000,
|
|
187
|
+
endTime: 2000,
|
|
188
|
+
attributes: { "http.method": "GET", "http.url": "/api/test" },
|
|
189
|
+
children: [],
|
|
190
|
+
}];
|
|
191
|
+
|
|
192
|
+
const output = formatSpanTree(spans);
|
|
193
|
+
|
|
194
|
+
// Should show spanId
|
|
195
|
+
expect(output).toContain("[spanId:span_1]");
|
|
196
|
+
// Should NOT show attributes (simplified output)
|
|
197
|
+
expect(output).not.toContain("http.method");
|
|
198
|
+
expect(output).not.toContain("GET");
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe("GREEN: TraceCommand structure", () => {
|
|
203
|
+
test("should have correct command name 'trace'", async () => {
|
|
204
|
+
const { TraceCommand } = await import("./index");
|
|
205
|
+
|
|
206
|
+
expect(TraceCommand.command).toBe("trace");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("should have describe text mentioning tree visualization", async () => {
|
|
210
|
+
const { TraceCommand } = await import("./index");
|
|
211
|
+
|
|
212
|
+
expect(TraceCommand.describe).toBeDefined();
|
|
213
|
+
expect(typeof TraceCommand.describe).toBe("string");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("should have builder that adds traceId positional argument", async () => {
|
|
217
|
+
const { TraceCommand } = await import("./index");
|
|
218
|
+
|
|
219
|
+
let positionalConfig: any = null;
|
|
220
|
+
const mockYargs = {
|
|
221
|
+
positional: (name: string, config: any) => {
|
|
222
|
+
if (name === "traceId") {
|
|
223
|
+
positionalConfig = config;
|
|
224
|
+
}
|
|
225
|
+
return mockYargs;
|
|
226
|
+
},
|
|
227
|
+
option: () => mockYargs,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
TraceCommand.builder(mockYargs as any);
|
|
231
|
+
|
|
232
|
+
expect(positionalConfig).toBeDefined();
|
|
233
|
+
expect(positionalConfig.type).toBe("string");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("should have --db option for custom db path", async () => {
|
|
237
|
+
const { TraceCommand } = await import("./index");
|
|
238
|
+
|
|
239
|
+
let capturedOptions: Record<string, any> = {};
|
|
240
|
+
const mockYargs = {
|
|
241
|
+
option: (name: string, config: any) => {
|
|
242
|
+
capturedOptions[name] = config;
|
|
243
|
+
return mockYargs;
|
|
244
|
+
},
|
|
245
|
+
positional: () => mockYargs,
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
TraceCommand.builder(mockYargs as any);
|
|
249
|
+
|
|
250
|
+
expect(capturedOptions.db).toBeDefined();
|
|
251
|
+
expect(capturedOptions.db.type).toBe("string");
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Debug Trace Command
|
|
3
|
+
*
|
|
4
|
+
* Command: roy debug trace <traceId>
|
|
5
|
+
*
|
|
6
|
+
* 以 tree 方式展示 traceId 的 spans
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as path from "path";
|
|
10
|
+
import * as os from "os";
|
|
11
|
+
import type { CommandModule } from "yargs";
|
|
12
|
+
import { SQLiteSpanStorage } from "@ai-setting/roy-agent-core";
|
|
13
|
+
import type { Span } from "@ai-setting/roy-agent-core";
|
|
14
|
+
|
|
15
|
+
interface TraceOptions {
|
|
16
|
+
traceId?: string;
|
|
17
|
+
db?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const TraceCommand: CommandModule<object, TraceOptions> = {
|
|
21
|
+
command: "trace",
|
|
22
|
+
describe: "以 tree 方式查看 trace 的 spans",
|
|
23
|
+
|
|
24
|
+
builder: (yargs) =>
|
|
25
|
+
yargs
|
|
26
|
+
.positional("traceId", {
|
|
27
|
+
type: "string",
|
|
28
|
+
describe: "Trace ID",
|
|
29
|
+
demandOption: true,
|
|
30
|
+
})
|
|
31
|
+
.option("db", {
|
|
32
|
+
type: "string",
|
|
33
|
+
describe: "SQLite 数据库路径(可选,默认使用配置路径)",
|
|
34
|
+
}),
|
|
35
|
+
|
|
36
|
+
async handler(args) {
|
|
37
|
+
const traceId = args._[2] as string || args.traceId;
|
|
38
|
+
|
|
39
|
+
if (!traceId) {
|
|
40
|
+
console.error("Error: traceId is required");
|
|
41
|
+
console.error("Usage: roy debug trace <traceId>");
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 获取数据库路径
|
|
46
|
+
const dbPath = args.db || getDefaultSpanDbPath();
|
|
47
|
+
|
|
48
|
+
// 创建存储实例并读取数据
|
|
49
|
+
const storage = new SQLiteSpanStorage(dbPath);
|
|
50
|
+
await storage.initialize();
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
// 查询 spans
|
|
54
|
+
const spans = storage.findByTraceId(traceId);
|
|
55
|
+
|
|
56
|
+
if (spans.length === 0) {
|
|
57
|
+
console.log(`Trace ${traceId} not found.`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 格式化输出为 tree
|
|
62
|
+
// 注意:spans 已经是 buildTree() 返回的根节点数组,每个节点有 children
|
|
63
|
+
const treeOutput = formatSpanTree(spans);
|
|
64
|
+
|
|
65
|
+
// 分段输出到 stderr,避免被 log-trace 的 maxOutput 截断
|
|
66
|
+
const chunkSize = 4000;
|
|
67
|
+
for (let i = 0; i < treeOutput.length; i += chunkSize) {
|
|
68
|
+
console.error(treeOutput.substring(i, i + chunkSize));
|
|
69
|
+
}
|
|
70
|
+
} finally {
|
|
71
|
+
storage.close();
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 获取默认的 span 数据库路径
|
|
78
|
+
*/
|
|
79
|
+
export function getDefaultSpanDbPath(): string {
|
|
80
|
+
// 支持环境变量覆盖
|
|
81
|
+
if (process.env.LOG_TRACE_TRACING_DB_PATH) {
|
|
82
|
+
return process.env.LOG_TRACE_TRACING_DB_PATH;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 使用 XDG 标准路径
|
|
86
|
+
const home = os.homedir();
|
|
87
|
+
const dataHome = process.env.XDG_DATA_HOME || path.join(home, ".local", "share");
|
|
88
|
+
return path.join(dataHome, "roy-agent", "traces.db");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 格式化 spans 为 tree 视图
|
|
93
|
+
*
|
|
94
|
+
* spans 是已经构建好的树结构(根节点数组,每个节点有 children)
|
|
95
|
+
* 直接遍历输出即可
|
|
96
|
+
*/
|
|
97
|
+
export function formatSpanTree(spans: Span[]): string {
|
|
98
|
+
if (spans.length === 0) {
|
|
99
|
+
return "(empty)";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// spans 已经是根节点数组,直接格式化
|
|
103
|
+
const lines: string[] = [];
|
|
104
|
+
|
|
105
|
+
for (const root of spans) {
|
|
106
|
+
formatSpanNode(root, 0, lines);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return lines.join("\n");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 递归格式化单个 span
|
|
114
|
+
*
|
|
115
|
+
* 简化输出:只显示 name、duration、error、spanId
|
|
116
|
+
*/
|
|
117
|
+
function formatSpanNode(span: Span, depth: number, lines: string[]): void {
|
|
118
|
+
const indent = " ".repeat(depth);
|
|
119
|
+
const connector = depth === 0 ? "" : "└── ";
|
|
120
|
+
|
|
121
|
+
// 计算耗时
|
|
122
|
+
const duration = span.endTime
|
|
123
|
+
? `${span.endTime - span.startTime}ms`
|
|
124
|
+
: "(running)";
|
|
125
|
+
|
|
126
|
+
// 构建错误信息
|
|
127
|
+
const errorInfo = span.error
|
|
128
|
+
? ` ⚠ ${span.error}`
|
|
129
|
+
: "";
|
|
130
|
+
|
|
131
|
+
// 添加 spanId 显示
|
|
132
|
+
const spanIdInfo = `[spanId:${span.spanId}]`;
|
|
133
|
+
|
|
134
|
+
lines.push(`${indent}${connector}${span.name} (${duration})${errorInfo} ${spanIdInfo}`);
|
|
135
|
+
|
|
136
|
+
// 递归处理子 span
|
|
137
|
+
for (const child of span.children || []) {
|
|
138
|
+
formatSpanNode(child, depth + 1, lines);
|
|
139
|
+
}
|
|
140
|
+
}
|