gaoding-cli 1.0.0-alpha.13 → 1.0.0-alpha.15

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 (41) hide show
  1. package/README.md +2 -2
  2. package/contracts/operations/agent.send/input.schema.json +4 -0
  3. package/contracts/operations/model.get/output.schema.json +1 -0
  4. package/dist/bin/gd-cli.js +37 -16
  5. package/dist/src/bootstrap/create-cli.js +43 -4
  6. package/dist/src/bootstrap/create-runtime.js +44 -13
  7. package/dist/src/cli/action-binding.js +43 -9
  8. package/dist/src/cli/agent-commands.js +13 -3
  9. package/dist/src/cli/auth-commands.js +21 -13
  10. package/dist/src/cli/dam-commands.js +35 -20
  11. package/dist/src/cli/errors.js +34 -1
  12. package/dist/src/cli/model-commands.js +25 -11
  13. package/dist/src/cli/org-commands.js +18 -12
  14. package/dist/src/cli/presenter.js +11 -1
  15. package/dist/src/cli/tool-commands.js +19 -7
  16. package/dist/src/cli/update-command.js +6 -4
  17. package/dist/src/features/agent/creative-agent-adapter.js +19 -14
  18. package/dist/src/features/agent/creative-protocol.js +7 -3
  19. package/dist/src/features/agent/use-cases.js +63 -54
  20. package/dist/src/features/auth/use-cases.js +82 -65
  21. package/dist/src/features/dam/asset-projection.js +22 -14
  22. package/dist/src/features/dam/dam-api-adapter.js +19 -4
  23. package/dist/src/features/dam/object-storage.js +2 -0
  24. package/dist/src/features/dam/registered-uploader.js +58 -35
  25. package/dist/src/features/dam/use-cases.js +39 -32
  26. package/dist/src/features/org/use-cases.js +27 -11
  27. package/dist/src/features/tool/catalog.js +7 -3
  28. package/dist/src/features/tool/dynamic-schema.js +19 -9
  29. package/dist/src/features/tool/mns-catalog-adapter.js +7 -4
  30. package/dist/src/features/tool/tool-api-adapter.js +186 -73
  31. package/dist/src/features/tool/use-cases.js +63 -34
  32. package/dist/src/features/update/update-service.js +28 -22
  33. package/dist/src/platform/json-input.js +3 -0
  34. package/dist/src/platform/remote-error-evidence.js +66 -0
  35. package/dist/src/platform/remote-protocol.js +10 -0
  36. package/dist/src/platform/signed-http-transport.js +31 -5
  37. package/dist/src/telemetry/invocation.js +156 -0
  38. package/dist/src/telemetry/sls-sink.js +31 -0
  39. package/package.json +1 -1
  40. package/skills/gd-cli/references/creation.md +15 -4
  41. package/skills/gd-cli/references/errors.md +3 -1
package/README.md CHANGED
@@ -28,10 +28,10 @@ gd-cli org switch --org <org-id>
28
28
  gd-cli agent send --schema
29
29
  gd-cli tool list
30
30
  gd-cli model list --tool <tool>
31
- gd-cli tool call <tool> --schema
31
+ gd-cli model get <model>
32
32
  ```
33
33
 
34
- 运行 `gd-cli --help` 查看当前命令;运行任一命令的 `--help` 查看参数。`gd-cli update` 固定检查 npm `latest`,支持更新 npm、pnpm 全局安装并同步 Agent Skill。
34
+ 选定 Model 后,以 `model get` 返回的 `parameters` 与 `usageDescription` 构造实际输入;`tool call --schema` 只给出整个 Tool 的参数并集。运行 `gd-cli --help` 查看当前命令;运行任一命令的 `--help` 查看参数。`gd-cli update` 固定检查 npm `latest`,支持更新 npm、pnpm 全局安装并同步 Agent Skill。
35
35
 
36
36
  ## License
37
37
 
@@ -123,6 +123,10 @@
123
123
  "properties": {
124
124
  "contextId": {
125
125
  "type": "string"
126
+ },
127
+ "parts": {
128
+ "type": "array",
129
+ "maxItems": 1
126
130
  }
127
131
  },
128
132
  "required": [
@@ -127,6 +127,7 @@
127
127
  "type": "string",
128
128
  "enum": [
129
129
  "string",
130
+ "number",
130
131
  "uri",
131
132
  "uri[]"
132
133
  ],
@@ -1,38 +1,59 @@
1
1
  #!/usr/bin/env node
2
- import { createCli, executeCli } from "../src/bootstrap/create-cli.js";
3
- import { createProductionRuntime } from "../src/bootstrap/create-runtime.js";
4
- import { mapCliError } from "../src/cli/errors.js";
5
- import { redact } from "../src/platform/redact.js";
6
2
  const controller = new AbortController();
7
3
  const interrupt = () => controller.abort(new Error("SIGINT"));
8
4
  process.once("SIGINT", interrupt);
9
5
  let runtime;
6
+ let mapCliError;
7
+ let redact;
10
8
  try {
9
+ // Register SIGINT before loading the CLI graph so a cold-start interrupt is
10
+ // converted into the same abort path as an interrupt during stdin reading.
11
+ const [cliModule, runtimeModule, errorsModule, redactModule] = await Promise.all([
12
+ import("../src/bootstrap/create-cli.js"),
13
+ import("../src/bootstrap/create-runtime.js"),
14
+ import("../src/cli/errors.js"),
15
+ import("../src/platform/redact.js")
16
+ ]);
17
+ const { createCli, executeCli } = cliModule;
18
+ const { createProductionRuntime } = runtimeModule;
19
+ mapCliError = errorsModule.mapCliError;
20
+ redact = redactModule.redact;
11
21
  runtime = createProductionRuntime();
12
22
  const program = createCli(runtime, { signal: controller.signal });
13
23
  process.exitCode = await executeCli({
14
24
  program,
15
25
  argv: process.argv.slice(2),
16
26
  presenter: runtime.presenter,
17
- signal: controller.signal
27
+ signal: controller.signal,
28
+ telemetry: runtime.telemetry,
29
+ telemetrySink: runtime.telemetrySink
18
30
  });
19
31
  }
20
32
  catch (error) {
21
- const outcome = redact(mapCliError(error, { aborted: controller.signal.aborted }));
22
- if (runtime) {
23
- runtime.presenter.error(outcome, { json: process.argv.includes("--json") });
24
- }
25
- else if (outcome.kind === "interrupted") {
26
- process.stderr.write("已取消。\n");
27
- }
28
- else if (process.argv.includes("--json")) {
29
- process.stderr.write(`${JSON.stringify({ error: outcome.failure })}\n`);
33
+ // Dynamic module loading can fail before the error mapper is available.
34
+ // Keep that rare startup failure bounded and avoid an unhandled rejection.
35
+ if (mapCliError === undefined || redact === undefined) {
36
+ process.stderr.write("INTERNAL_ERROR: CLI 启动失败。\n");
37
+ process.exitCode = 1;
30
38
  }
31
39
  else {
32
- process.stderr.write(`${outcome.failure.code}: ${outcome.failure.message}\n`);
40
+ const outcome = redact(mapCliError(error, { aborted: controller.signal.aborted }));
41
+ if (runtime) {
42
+ runtime.presenter.error(outcome, { json: process.argv.includes("--json") });
43
+ }
44
+ else if (outcome.kind === "interrupted") {
45
+ process.stderr.write("已取消。\n");
46
+ }
47
+ else if (process.argv.includes("--json")) {
48
+ process.stderr.write(`${JSON.stringify({ error: outcome.failure })}\n`);
49
+ }
50
+ else {
51
+ process.stderr.write(`${outcome.failure.code}: ${outcome.failure.message}\n`);
52
+ }
53
+ process.exitCode = outcome.exitCode;
33
54
  }
34
- process.exitCode = outcome.exitCode;
35
55
  }
36
56
  finally {
37
57
  process.removeListener("SIGINT", interrupt);
38
58
  }
59
+ export {};
@@ -1,8 +1,8 @@
1
- import { Command } from "commander";
1
+ import { Command, CommanderError } from "commander";
2
2
  import { assertAllLeafCommandsBound, configureActionEnvironment } from "../cli/action-binding.js";
3
3
  import { registerAgentCommands } from "../cli/agent-commands.js";
4
4
  import { registerAuthCommands } from "../cli/auth-commands.js";
5
- import { isHelpOutcome, mapCliError } from "../cli/errors.js";
5
+ import { classifyTelemetryError, isHelpOutcome, mapCliError } from "../cli/errors.js";
6
6
  import { registerOrgCommands } from "../cli/org-commands.js";
7
7
  import { registerToolCommands } from "../cli/tool-commands.js";
8
8
  import { registerModelCommands } from "../cli/model-commands.js";
@@ -27,7 +27,8 @@ export function createCli(runtime, options) {
27
27
  });
28
28
  configureActionEnvironment(program, {
29
29
  policies: runtime.accessPolicies,
30
- signal: options.signal
30
+ signal: options.signal,
31
+ telemetry: runtime.telemetry
31
32
  });
32
33
  registerAuthCommands(program, runtime);
33
34
  registerOrgCommands(program, runtime);
@@ -43,13 +44,51 @@ export function createCli(runtime, options) {
43
44
  export async function executeCli(options) {
44
45
  try {
45
46
  await options.program.parseAsync(options.argv, { from: "user" });
47
+ await sendFinished(options, { outcome: "success" });
46
48
  return 0;
47
49
  }
48
50
  catch (error) {
49
51
  if (isHelpOutcome(error))
50
52
  return 0;
53
+ if (options.telemetry.selected && error instanceof CommanderError) {
54
+ try {
55
+ await options.telemetry.stage("input", () => { throw error; });
56
+ }
57
+ catch {
58
+ // The original Commander error is mapped and presented below.
59
+ }
60
+ }
51
61
  const outcome = redact(mapCliError(error, { aborted: options.signal.aborted }));
52
- options.presenter.error(outcome, { json: options.argv.includes("--json") });
62
+ const telemetryError = classifyTelemetryError(error);
63
+ const traceId = options.telemetry.selected
64
+ ? options.telemetry.traceId
65
+ : undefined;
66
+ options.presenter.error(outcome, {
67
+ json: options.argv.includes("--json"),
68
+ ...(traceId ? { traceId } : {})
69
+ });
70
+ await sendFinished(options, outcome.kind === "interrupted"
71
+ ? { outcome: "interrupted", error }
72
+ : {
73
+ outcome: "failure",
74
+ error,
75
+ errorCode: outcome.failure.code,
76
+ errorMessage: outcome.failure.message,
77
+ ...(telemetryError.httpStatus === undefined
78
+ ? {}
79
+ : { httpStatus: telemetryError.httpStatus }),
80
+ ...(telemetryError.remoteErrorCode === undefined
81
+ ? {}
82
+ : { remoteErrorCode: telemetryError.remoteErrorCode }),
83
+ ...(telemetryError.remoteErrorBody === undefined
84
+ ? {}
85
+ : { remoteErrorBody: telemetryError.remoteErrorBody })
86
+ });
53
87
  return outcome.exitCode;
54
88
  }
55
89
  }
90
+ async function sendFinished(options, completion) {
91
+ const events = options.telemetry.finish(completion);
92
+ if (events.length > 0)
93
+ await options.telemetrySink.send(events);
94
+ }
@@ -1,4 +1,4 @@
1
- import { randomUUID } from "node:crypto";
1
+ import { randomBytes, randomUUID } from "node:crypto";
2
2
  import { dirname } from "node:path";
3
3
  import { setTimeout as delay } from "node:timers/promises";
4
4
  import { fileURLToPath } from "node:url";
@@ -30,6 +30,9 @@ import { createPresenter } from "../cli/presenter.js";
30
30
  import { createContractValidators } from "./validators.js";
31
31
  import { loadBundledSkills } from "../features/skill/bundled-skills.js";
32
32
  import { createUpdateService } from "../features/update/update-service.js";
33
+ import { classifyTelemetryError } from "../cli/errors.js";
34
+ import { createTelemetryInvocation } from "../telemetry/invocation.js";
35
+ import { createSlsTelemetrySink } from "../telemetry/sls-sink.js";
33
36
  const productionEndpoints = {
34
37
  orgApi: new URL("https://gdcli.gaoding.com/api"),
35
38
  agentApi: new URL("https://gdcli.gaoding.com/api"),
@@ -47,6 +50,19 @@ export function createProductionRuntime(options = {}) {
47
50
  const endpoints = options.endpoints ?? productionEndpoints;
48
51
  const fetch = options.fetch ?? globalThis.fetch;
49
52
  const now = options.now ?? (() => new Date());
53
+ const telemetry = createTelemetryInvocation({
54
+ cliVersion: primarySkill.version,
55
+ now: () => now().getTime(),
56
+ traceIdFactory: () => randomBytes(16).toString("hex"),
57
+ spanIdFactory: () => randomBytes(8).toString("hex"),
58
+ runtime: {
59
+ os: process.platform,
60
+ arch: process.arch,
61
+ nodeMajor: Number.parseInt(process.versions.node, 10)
62
+ },
63
+ classifyError: classifyTelemetryError
64
+ });
65
+ const telemetrySink = createSlsTelemetrySink({ fetch });
50
66
  const rawIo = options.io ?? {
51
67
  input: process.stdin,
52
68
  stdout: process.stdout,
@@ -66,28 +82,33 @@ export function createProductionRuntime(options = {}) {
66
82
  const orgTransport = createSignedHttpTransport({
67
83
  baseUrl: endpoints.orgApi,
68
84
  fetch,
69
- now
85
+ now,
86
+ traceparent: () => telemetry.traceparent()
70
87
  });
71
88
  const agentTransport = createSignedHttpTransport({
72
89
  baseUrl: endpoints.agentApi,
73
90
  fetch,
74
- now
91
+ now,
92
+ traceparent: () => telemetry.traceparent()
75
93
  });
76
94
  const mnsTransport = createSignedHttpTransport({
77
95
  baseUrl: endpoints.mnsApi,
78
96
  fetch,
79
- now
97
+ now,
98
+ traceparent: () => telemetry.traceparent()
80
99
  });
81
100
  const toolTransport = createSignedHttpTransport({
82
101
  baseUrl: endpoints.toolApi,
83
102
  fetch,
84
- now
103
+ now,
104
+ traceparent: () => telemetry.traceparent()
85
105
  });
86
106
  const damTransport = createSignedHttpTransport({
87
107
  baseUrl: endpoints.damApi,
88
108
  fetch,
89
109
  now,
90
- channelId: "32"
110
+ channelId: "32",
111
+ traceparent: () => telemetry.traceparent()
91
112
  });
92
113
  const sso = createSsoService({
93
114
  ssoApi: endpoints.ssoApi,
@@ -97,7 +118,8 @@ export function createProductionRuntime(options = {}) {
97
118
  const org = createOrgUseCases({
98
119
  store,
99
120
  org: createOrgService({ transport: orgTransport }),
100
- sso
121
+ sso,
122
+ telemetry
101
123
  });
102
124
  const auth = createAuthUseCases({
103
125
  store,
@@ -105,6 +127,7 @@ export function createProductionRuntime(options = {}) {
105
127
  organizationBinder: org,
106
128
  authPageOrigin: endpoints.authPageOrigin,
107
129
  now,
130
+ telemetry,
108
131
  sleep: async (milliseconds, signal) => {
109
132
  await delay(milliseconds, undefined, { signal });
110
133
  }
@@ -129,7 +152,8 @@ export function createProductionRuntime(options = {}) {
129
152
  api: damApi,
130
153
  storage: storageUpload,
131
154
  warn: (message) => { presenter.warning(message); },
132
- now
155
+ now,
156
+ telemetry
133
157
  }),
134
158
  validate: {
135
159
  list: validators.damListOutput,
@@ -137,20 +161,24 @@ export function createProductionRuntime(options = {}) {
137
161
  get: validators.damGetOutput,
138
162
  upload: validators.damUploadOutput,
139
163
  delete: validators.damDeleteOutput
140
- }
164
+ },
165
+ telemetry
141
166
  });
142
167
  const agent = createAgentUseCases({
143
168
  uploader: transientUploader,
144
169
  completion: createCreativeAgentAdapter({
145
170
  transport: agentTransport,
146
- idFactory: randomUUID
147
- })
171
+ idFactory: randomUUID,
172
+ telemetry
173
+ }),
174
+ telemetry
148
175
  });
149
176
  const tool = createToolUseCases({
150
177
  catalog: createMnsCatalogAdapter({ transport: mnsTransport }),
151
- execution: createToolApiAdapter({ transport: toolTransport }),
178
+ execution: createToolApiAdapter({ transport: toolTransport, telemetry }),
152
179
  uploader: transientUploader,
153
- warn: (message) => { presenter.warning(message); }
180
+ warn: (message) => { presenter.warning(message); },
181
+ telemetry
154
182
  });
155
183
  const editor = createEditorUseCases({
156
184
  session: createEditorSession({
@@ -183,6 +211,7 @@ export function createProductionRuntime(options = {}) {
183
211
  packageRoot,
184
212
  bundledSkills,
185
213
  fetch,
214
+ telemetry,
186
215
  ...(process.argv[1] ? { binaryPath: process.argv[1] } : {}),
187
216
  ...(options.homeDirectory ? { homeDirectory: options.homeDirectory } : {})
188
217
  }).run(signal);
@@ -190,6 +219,8 @@ export function createProductionRuntime(options = {}) {
190
219
  };
191
220
  return {
192
221
  version: primarySkill.version,
222
+ telemetry,
223
+ telemetrySink,
193
224
  accessPolicies: createAccessPolicies({
194
225
  store,
195
226
  now,
@@ -3,34 +3,46 @@ const boundPolicies = new WeakMap();
3
3
  export function configureActionEnvironment(program, environment) {
4
4
  environments.set(program, environment);
5
5
  }
6
- export function bindAction(command, policy, handler) {
6
+ export function bindAction(command, policy, handler, telemetry) {
7
7
  if (boundPolicies.has(command))
8
8
  throw new Error(`Action already bound: ${commandPath(command)}`);
9
9
  boundPolicies.set(command, policy);
10
+ bindTelemetrySelection(command, telemetry);
10
11
  command.action(async () => {
11
12
  const environment = environments.get(rootCommand(command));
12
13
  if (!environment)
13
14
  throw new Error("CLI action environment is missing.");
14
- const context = await environment.policies[policy].authorize(environment.signal);
15
- if (context.policy !== policy)
16
- throw new Error("Access policy returned the wrong context.");
15
+ const context = await environment.telemetry.stage("access", async () => {
16
+ const authorized = await environment.policies[policy].authorize(environment.signal);
17
+ if (authorized.policy !== policy) {
18
+ throw new Error("Access policy returned the wrong context.");
19
+ }
20
+ return authorized;
21
+ });
22
+ annotateAccessContext(environment.telemetry, context);
17
23
  await handler(context, environment.signal);
18
24
  });
19
25
  }
20
- export function bindPreparedAction(command, policy, prepare, run) {
26
+ export function bindPreparedAction(command, policy, prepare, run, telemetry) {
21
27
  if (boundPolicies.has(command))
22
28
  throw new Error(`Action already bound: ${commandPath(command)}`);
23
29
  boundPolicies.set(command, policy);
30
+ bindTelemetrySelection(command, telemetry);
24
31
  command.action(async () => {
25
32
  const environment = environments.get(rootCommand(command));
26
33
  if (!environment)
27
34
  throw new Error("CLI action environment is missing.");
28
- const prepared = await prepare(environment.signal);
35
+ const prepared = await environment.telemetry.stage("input", () => prepare(environment.signal));
29
36
  if (prepared.handled)
30
37
  return;
31
- const context = await environment.policies[policy].authorize(environment.signal);
32
- if (context.policy !== policy)
33
- throw new Error("Access policy returned the wrong context.");
38
+ const context = await environment.telemetry.stage("access", async () => {
39
+ const authorized = await environment.policies[policy].authorize(environment.signal);
40
+ if (authorized.policy !== policy) {
41
+ throw new Error("Access policy returned the wrong context.");
42
+ }
43
+ return authorized;
44
+ });
45
+ annotateAccessContext(environment.telemetry, context);
34
46
  await run(context, prepared.input, environment.signal);
35
47
  });
36
48
  }
@@ -52,6 +64,28 @@ function rootCommand(command) {
52
64
  current = current.parent;
53
65
  return current;
54
66
  }
67
+ function bindTelemetrySelection(command, definition) {
68
+ if (!definition)
69
+ return;
70
+ if (!command.parent)
71
+ throw new Error(`Telemetry command has no parent: ${command.name()}`);
72
+ command.parent.hook("preSubcommand", (parent, selected) => {
73
+ if (selected !== command)
74
+ return;
75
+ const environment = environments.get(rootCommand(parent));
76
+ if (!environment)
77
+ throw new Error("CLI action environment is missing.");
78
+ environment.telemetry.select(definition);
79
+ });
80
+ }
81
+ function annotateAccessContext(telemetry, context) {
82
+ if (context.policy === "none")
83
+ return;
84
+ telemetry.annotate({
85
+ account_id: context.state.account?.id,
86
+ organization_id: context.state.organization?.id
87
+ });
88
+ }
55
89
  function commandPath(command) {
56
90
  const names = [];
57
91
  let current = command;
@@ -18,11 +18,14 @@ export function registerAgentCommands(program, runtime) {
18
18
  if (modes !== 1)
19
19
  throw new CliUsageError();
20
20
  if (options.schema === true) {
21
- runtime.io.writeOut(`${JSON.stringify(agentSendInputSchema)}\n`);
21
+ await runtime.telemetry.stage("output", async () => {
22
+ runtime.io.writeOut(`${JSON.stringify(agentSendInputSchema)}\n`);
23
+ });
22
24
  return { handled: true };
23
25
  }
24
26
  const input = await runtime.jsonInput.read(options.input, signal);
25
27
  runtime.validators.agentSendInput(input);
28
+ runtime.telemetry.annotate({ parameters: { ...input } });
26
29
  return { handled: false, input };
27
30
  }, async (context, input, signal) => {
28
31
  const result = await runtime.agent.send({
@@ -33,7 +36,14 @@ export function registerAgentCommands(program, runtime) {
33
36
  },
34
37
  signal
35
38
  });
36
- runtime.validators.agentSendOutput(result);
37
- runtime.io.writeOut(`${JSON.stringify(result)}\n`);
39
+ await runtime.telemetry.stage("output", async () => {
40
+ runtime.validators.agentSendOutput(result);
41
+ runtime.io.writeOut(`${JSON.stringify(result)}\n`);
42
+ });
43
+ }, {
44
+ operation: "agent.send",
45
+ mode: () => send.opts().schema === true
46
+ ? "schema"
47
+ : "execute"
38
48
  });
39
49
  }
@@ -8,8 +8,10 @@ export function registerAuthCommands(program, runtime) {
8
8
  .description("登录稿定")
9
9
  .option("--no-browser", "不自动打开浏览器");
10
10
  bindAction(login, "none", async (_context, signal) => {
11
+ const noBrowser = login.opts().browser === false;
12
+ runtime.telemetry.annotate({ parameters: { noBrowser } });
11
13
  const result = await runtime.auth.login({
12
- noBrowser: login.opts().browser === false,
14
+ noBrowser,
13
15
  signal,
14
16
  interaction: {
15
17
  showAuthorization: (url) => runtime.presenter.authorization({
@@ -20,27 +22,33 @@ export function registerAuthCommands(program, runtime) {
20
22
  warn: (message) => runtime.presenter.warning(message)
21
23
  }
22
24
  });
23
- runtime.presenter.success(result.status === "already_logged_in" ? "已登录。" : "登录成功。");
24
- if (result.status === "already_logged_in" && !result.organizationSelected) {
25
- runtime.presenter.warning("尚未选择组织,请执行 gd-cli org switch。");
26
- }
27
- });
25
+ await runtime.telemetry.stage("output", async () => {
26
+ runtime.presenter.success(result.status === "already_logged_in" ? "已登录。" : "登录成功。");
27
+ if (result.status === "already_logged_in" && !result.organizationSelected) {
28
+ runtime.presenter.warning("尚未选择组织,请执行 gd-cli org switch。");
29
+ }
30
+ });
31
+ }, { operation: "auth.login" });
28
32
  const status = leaf(auth.command("status"))
29
33
  .description("查看登录状态")
30
34
  .option("--json", "输出 JSON");
31
35
  bindAction(status, "none", async () => {
32
36
  const result = await runtime.auth.status({ now: runtime.now() });
33
- runtime.validators.authStatus(result);
34
- runtime.presenter.result(result, {
35
- json: status.opts().json === true,
36
- view: authStatusView
37
+ await runtime.telemetry.stage("output", async () => {
38
+ runtime.validators.authStatus(result);
39
+ runtime.presenter.result(result, {
40
+ json: status.opts().json === true,
41
+ view: authStatusView
42
+ });
37
43
  });
38
- });
44
+ }, { operation: "auth.status" });
39
45
  const logout = leaf(auth.command("logout")).description("退出登录");
40
46
  bindAction(logout, "none", async () => {
41
47
  await runtime.auth.logout();
42
- runtime.presenter.success("已退出登录。");
43
- });
48
+ await runtime.telemetry.stage("output", async () => {
49
+ runtime.presenter.success("已退出登录。");
50
+ });
51
+ }, { operation: "auth.logout" });
44
52
  }
45
53
  function leaf(command) {
46
54
  return command.allowExcessArguments(false);
@@ -10,6 +10,7 @@ export function registerDamCommands(program, runtime) {
10
10
  signal.throwIfAborted();
11
11
  const input = browseInput(list);
12
12
  runtime.validators.damListInput(input);
13
+ runtime.telemetry.annotate({ parameters: { ...input } });
13
14
  return { handled: false, input };
14
15
  }, async (context, input, signal) => {
15
16
  const result = await runtime.dam.list({
@@ -17,11 +18,13 @@ export function registerDamCommands(program, runtime) {
17
18
  access: organizationAccess(context.state),
18
19
  signal
19
20
  });
20
- runtime.presenter.result(result, {
21
- json: list.opts().json === true,
22
- view: assetListView
21
+ await runtime.telemetry.stage("output", async () => {
22
+ runtime.presenter.result(result, {
23
+ json: list.opts().json === true,
24
+ view: assetListView
25
+ });
23
26
  });
24
- });
27
+ }, { operation: "dam.list" });
25
28
  const search = browseCommand(leaf(dam.command("search")))
26
29
  .description("按关键词搜索素材")
27
30
  .argument("<query>", "搜索关键词");
@@ -32,6 +35,7 @@ export function registerDamCommands(program, runtime) {
32
35
  ...browseInput(search)
33
36
  };
34
37
  runtime.validators.damSearchInput(input);
38
+ runtime.telemetry.annotate({ parameters: { ...input } });
35
39
  return { handled: false, input };
36
40
  }, async (context, input, signal) => {
37
41
  const result = await runtime.dam.search({
@@ -39,11 +43,13 @@ export function registerDamCommands(program, runtime) {
39
43
  access: organizationAccess(context.state),
40
44
  signal
41
45
  });
42
- runtime.presenter.result(result, {
43
- json: search.opts().json === true,
44
- view: assetListView
46
+ await runtime.telemetry.stage("output", async () => {
47
+ runtime.presenter.result(result, {
48
+ json: search.opts().json === true,
49
+ view: assetListView
50
+ });
45
51
  });
46
- });
52
+ }, { operation: "dam.search" });
47
53
  const get = leaf(dam.command("get"))
48
54
  .description("查看素材完整信息")
49
55
  .argument("<asset>", "素材 ID")
@@ -52,6 +58,7 @@ export function registerDamCommands(program, runtime) {
52
58
  signal.throwIfAborted();
53
59
  const input = { asset_id: position(get, 0) };
54
60
  runtime.validators.damGetInput(input);
61
+ runtime.telemetry.annotate({ parameters: { ...input } });
55
62
  return { handled: false, input };
56
63
  }, async (context, input, signal) => {
57
64
  const result = await runtime.dam.get({
@@ -59,11 +66,13 @@ export function registerDamCommands(program, runtime) {
59
66
  access: organizationAccess(context.state),
60
67
  signal
61
68
  });
62
- runtime.presenter.result(result, {
63
- json: get.opts().json === true,
64
- view: assetDetailView
69
+ await runtime.telemetry.stage("output", async () => {
70
+ runtime.presenter.result(result, {
71
+ json: get.opts().json === true,
72
+ view: assetDetailView
73
+ });
65
74
  });
66
- });
75
+ }, { operation: "dam.get" });
67
76
  const upload = leaf(dam.command("upload"))
68
77
  .description("上传本地文件并创建素材")
69
78
  .argument("<file>", "本地文件路径")
@@ -87,6 +96,7 @@ export function registerDamCommands(program, runtime) {
87
96
  ...(options.waitAnalysis === true ? { waitAnalysis: true } : {})
88
97
  };
89
98
  runtime.validators.damUploadInput(input);
99
+ runtime.telemetry.annotate({ parameters: { ...input } });
90
100
  return { handled: false, input };
91
101
  }, async (context, input, signal) => {
92
102
  const result = await runtime.dam.upload({
@@ -94,11 +104,13 @@ export function registerDamCommands(program, runtime) {
94
104
  access: organizationAccess(context.state),
95
105
  signal
96
106
  });
97
- runtime.presenter.result(result, {
98
- json: upload.opts().json === true,
99
- view: assetDetailView
107
+ await runtime.telemetry.stage("output", async () => {
108
+ runtime.presenter.result(result, {
109
+ json: upload.opts().json === true,
110
+ view: assetDetailView
111
+ });
100
112
  });
101
- });
113
+ }, { operation: "dam.upload" });
102
114
  const deleteCommand = leaf(dam.command("delete"))
103
115
  .description("删除素材")
104
116
  .argument("<asset>", "素材 ID")
@@ -113,6 +125,7 @@ export function registerDamCommands(program, runtime) {
113
125
  ...(options.permanent === true ? { permanent: true } : {})
114
126
  };
115
127
  runtime.validators.damDeleteInput(input);
128
+ runtime.telemetry.annotate({ parameters: { ...input } });
116
129
  return { handled: false, input };
117
130
  }, async (context, input, signal) => {
118
131
  await runtime.dam.delete({
@@ -120,10 +133,12 @@ export function registerDamCommands(program, runtime) {
120
133
  access: organizationAccess(context.state),
121
134
  signal
122
135
  });
123
- runtime.presenter.success(input.permanent === true
124
- ? "素材已永久删除。"
125
- : "素材已移入回收站。");
126
- });
136
+ await runtime.telemetry.stage("output", async () => {
137
+ runtime.presenter.success(input.permanent === true
138
+ ? "素材已永久删除。"
139
+ : "素材已移入回收站。");
140
+ });
141
+ }, { operation: "dam.delete" });
127
142
  }
128
143
  function leaf(command) {
129
144
  return command.allowExcessArguments(false);