gaoding-cli 1.0.0-alpha.14 → 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.
package/README.md CHANGED
@@ -29,10 +29,9 @@ gd-cli agent send --schema
29
29
  gd-cli tool list
30
30
  gd-cli model list --tool <tool>
31
31
  gd-cli model get <model>
32
- gd-cli model get <model> --schema
33
32
  ```
34
33
 
35
- 选定 Model 后,`model get --schema` 给出该 Model 的实际运行时输入;`tool call --schema` 给出整个 Tool 的参数并集。运行 `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。
36
35
 
37
36
  ## License
38
37
 
@@ -1,13 +1,23 @@
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({
@@ -20,21 +30,30 @@ try {
20
30
  });
21
31
  }
22
32
  catch (error) {
23
- const outcome = redact(mapCliError(error, { aborted: controller.signal.aborted }));
24
- if (runtime) {
25
- runtime.presenter.error(outcome, { json: process.argv.includes("--json") });
26
- }
27
- else if (outcome.kind === "interrupted") {
28
- process.stderr.write("已取消。\n");
29
- }
30
- else if (process.argv.includes("--json")) {
31
- 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;
32
38
  }
33
39
  else {
34
- 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;
35
54
  }
36
- process.exitCode = outcome.exitCode;
37
55
  }
38
56
  finally {
39
57
  process.removeListener("SIGINT", interrupt);
40
58
  }
59
+ export {};
@@ -2,7 +2,7 @@ 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";
@@ -10,7 +10,6 @@ import { registerEditorCommands } from "../cli/editor-commands.js";
10
10
  import { registerDamCommands } from "../cli/dam-commands.js";
11
11
  import { registerUpdateCommand } from "../cli/update-command.js";
12
12
  import { redact } from "../platform/redact.js";
13
- import { RemoteRequestError } from "../platform/signed-http-transport.js";
14
13
  export { assertAllLeafCommandsBound, bindAction } from "../cli/action-binding.js";
15
14
  export function createCli(runtime, options) {
16
15
  const program = new Command()
@@ -60,12 +59,13 @@ export async function executeCli(options) {
60
59
  }
61
60
  }
62
61
  const outcome = redact(mapCliError(error, { aborted: options.signal.aborted }));
63
- const invocationId = options.telemetry.selected
64
- ? options.telemetry.invocationId
62
+ const telemetryError = classifyTelemetryError(error);
63
+ const traceId = options.telemetry.selected
64
+ ? options.telemetry.traceId
65
65
  : undefined;
66
66
  options.presenter.error(outcome, {
67
67
  json: options.argv.includes("--json"),
68
- ...(invocationId ? { invocationId } : {})
68
+ ...(traceId ? { traceId } : {})
69
69
  });
70
70
  await sendFinished(options, outcome.kind === "interrupted"
71
71
  ? { outcome: "interrupted", error }
@@ -74,9 +74,15 @@ export async function executeCli(options) {
74
74
  error,
75
75
  errorCode: outcome.failure.code,
76
76
  errorMessage: outcome.failure.message,
77
- ...(error instanceof RemoteRequestError && error.status !== undefined
78
- ? { httpStatus: error.status }
79
- : {})
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 })
80
86
  });
81
87
  return outcome.exitCode;
82
88
  }
@@ -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";
@@ -53,7 +53,8 @@ export function createProductionRuntime(options = {}) {
53
53
  const telemetry = createTelemetryInvocation({
54
54
  cliVersion: primarySkill.version,
55
55
  now: () => now().getTime(),
56
- idFactory: randomUUID,
56
+ traceIdFactory: () => randomBytes(16).toString("hex"),
57
+ spanIdFactory: () => randomBytes(8).toString("hex"),
57
58
  runtime: {
58
59
  os: process.platform,
59
60
  arch: process.arch,
@@ -81,28 +82,33 @@ export function createProductionRuntime(options = {}) {
81
82
  const orgTransport = createSignedHttpTransport({
82
83
  baseUrl: endpoints.orgApi,
83
84
  fetch,
84
- now
85
+ now,
86
+ traceparent: () => telemetry.traceparent()
85
87
  });
86
88
  const agentTransport = createSignedHttpTransport({
87
89
  baseUrl: endpoints.agentApi,
88
90
  fetch,
89
- now
91
+ now,
92
+ traceparent: () => telemetry.traceparent()
90
93
  });
91
94
  const mnsTransport = createSignedHttpTransport({
92
95
  baseUrl: endpoints.mnsApi,
93
96
  fetch,
94
- now
97
+ now,
98
+ traceparent: () => telemetry.traceparent()
95
99
  });
96
100
  const toolTransport = createSignedHttpTransport({
97
101
  baseUrl: endpoints.toolApi,
98
102
  fetch,
99
- now
103
+ now,
104
+ traceparent: () => telemetry.traceparent()
100
105
  });
101
106
  const damTransport = createSignedHttpTransport({
102
107
  baseUrl: endpoints.damApi,
103
108
  fetch,
104
109
  now,
105
- channelId: "32"
110
+ channelId: "32",
111
+ traceparent: () => telemetry.traceparent()
106
112
  });
107
113
  const sso = createSsoService({
108
114
  ssoApi: endpoints.ssoApi,
@@ -5,6 +5,7 @@ import { SsoRequestError } from "../features/auth/sso-service.js";
5
5
  import { DeviceAuthorizationExpiredError } from "../features/auth/use-cases.js";
6
6
  import { AgentInputError } from "../features/agent/use-cases.js";
7
7
  import { ToolInputError } from "../features/tool/catalog.js";
8
+ import { ToolTaskFailedError } from "../features/tool/tool-api-adapter.js";
8
9
  import { ObjectStorageUploadError } from "../features/dam/object-storage.js";
9
10
  import { DamAssetNotFoundError } from "../features/dam/dam-api-adapter.js";
10
11
  import { DamInputError } from "../features/dam/use-cases.js";
@@ -64,6 +65,9 @@ export function mapCliError(error, options) {
64
65
  if (error instanceof RemoteProtocolError) {
65
66
  return failure("REMOTE_PROTOCOL_INCOMPATIBLE", "稿定服务返回了当前 CLI 无法安全处理的结果;为避免重复创作,已停止处理。", 1);
66
67
  }
68
+ if (error instanceof ToolTaskFailedError) {
69
+ return failure("TOOL_TASK_FAILED", "稿定工具任务执行失败。", 1);
70
+ }
67
71
  if (error instanceof DamAssetNotFoundError) {
68
72
  return failure("DAM_ASSET_NOT_FOUND", "未找到指定的 DAM 素材。", 1);
69
73
  }
@@ -87,12 +91,23 @@ export function mapCliError(error, options) {
87
91
  }
88
92
  export function classifyTelemetryError(error) {
89
93
  const outcome = mapCliError(error, { aborted: false });
94
+ const remoteError = error instanceof RemoteRequestError
95
+ || error instanceof RemoteProtocolError
96
+ || error instanceof ToolTaskFailedError
97
+ ? error
98
+ : undefined;
90
99
  return {
91
100
  outcome: outcome.kind === "interrupted" ? "interrupted" : "failure",
92
101
  ...(outcome.kind === "failure" ? { errorCode: outcome.failure.code } : {}),
93
102
  ...(error instanceof RemoteRequestError && error.status !== undefined
94
103
  ? { httpStatus: error.status }
95
- : {})
104
+ : {}),
105
+ ...(remoteError?.remoteCode === undefined
106
+ ? {}
107
+ : { remoteErrorCode: remoteError.remoteCode }),
108
+ ...(remoteError?.remoteBody === undefined
109
+ ? {}
110
+ : { remoteErrorBody: remoteError.remoteBody })
96
111
  };
97
112
  }
98
113
  function failure(code, message, exitCode, nextSteps) {
@@ -32,7 +32,6 @@ export function registerModelCommands(program, runtime) {
32
32
  const get = model.command("get")
33
33
  .description("查看模型完整信息")
34
34
  .argument("<model>", "模型机器标识")
35
- .option("--schema", "输出所选模型的实际运行时输入 Schema")
36
35
  .allowExcessArguments(false);
37
36
  bindAction(get, "organization", async (context, signal) => {
38
37
  const modelName = get.processedArgs[0];
@@ -46,18 +45,6 @@ export function registerModelCommands(program, runtime) {
46
45
  credential: context.state.credential,
47
46
  organizationId: context.state.organization.id
48
47
  };
49
- if (get.opts().schema === true) {
50
- const schema = await runtime.tool.getModelArgumentsSchema({
51
- model: modelName,
52
- access,
53
- signal
54
- });
55
- runtime.telemetry.annotate({ model_id: modelName });
56
- await runtime.telemetry.stage("output", async () => {
57
- runtime.io.writeOut(`${JSON.stringify(schema)}\n`);
58
- });
59
- return;
60
- }
61
48
  const result = await runtime.tool.getModel({
62
49
  input,
63
50
  access,
@@ -69,9 +56,6 @@ export function registerModelCommands(program, runtime) {
69
56
  runtime.io.writeOut(`${JSON.stringify(result)}\n`);
70
57
  });
71
58
  }, {
72
- operation: "model.get",
73
- mode: () => get.opts().schema === true
74
- ? "schema"
75
- : "detail"
59
+ operation: "model.get"
76
60
  });
77
61
  }
@@ -30,8 +30,8 @@ export function createPresenter(options) {
30
30
  options.writeError(`${JSON.stringify({
31
31
  error: {
32
32
  ...outcome.failure,
33
- ...(presentation.invocationId
34
- ? { invocation_id: presentation.invocationId }
33
+ ...(presentation.traceId
34
+ ? { trace_id: presentation.traceId }
35
35
  : {})
36
36
  }
37
37
  })}\n`);
@@ -42,8 +42,8 @@ export function createPresenter(options) {
42
42
  if (nextSteps.length > 0) {
43
43
  options.writeError(`下一步: ${nextSteps.join(",")}\n`);
44
44
  }
45
- if (presentation.invocationId) {
46
- options.writeError(`诊断 ID: ${presentation.invocationId}\n`);
45
+ if (presentation.traceId) {
46
+ options.writeError(`诊断 ID: ${presentation.traceId}\n`);
47
47
  }
48
48
  }
49
49
  };
@@ -1,6 +1,8 @@
1
1
  import { randomInt } from "node:crypto";
2
2
  import { setTimeout as delay } from "node:timers/promises";
3
3
  import { RemoteRequestError } from "../../platform/signed-http-transport.js";
4
+ import { remoteErrorEvidence } from "../../platform/remote-error-evidence.js";
5
+ import { RemoteProtocolError } from "../../platform/remote-protocol.js";
4
6
  const MAX_JAVA_LONG = 9223372036854775807n;
5
7
  function isRecord(value) {
6
8
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -8,6 +10,9 @@ function isRecord(value) {
8
10
  function fail() {
9
11
  throw new RemoteRequestError();
10
12
  }
13
+ function protocolFailure(value) {
14
+ throw new RemoteProtocolError(remoteErrorEvidence(value));
15
+ }
11
16
  function nonBlank(value) {
12
17
  if (typeof value !== "string" || value.trim() === "")
13
18
  return fail();
@@ -43,9 +48,17 @@ function parseIntent(value) {
43
48
  };
44
49
  }
45
50
  function submittedTask(value) {
46
- if (!isRecord(value) || !isRecord(value.result) || value.result.isError !== false)
47
- return fail();
48
- return nonBlank(value.task_id);
51
+ if (!isRecord(value) || !isRecord(value.result))
52
+ return protocolFailure(value);
53
+ if (value.result.isError === true) {
54
+ throw new ToolTaskFailedError(remoteErrorEvidence(value));
55
+ }
56
+ if (value.result.isError !== false)
57
+ return protocolFailure(value);
58
+ if (typeof value.task_id !== "string" || value.task_id.trim() === "") {
59
+ return protocolFailure(value);
60
+ }
61
+ return value.task_id;
49
62
  }
50
63
  function resourceName(uri) {
51
64
  let url;
@@ -53,32 +66,42 @@ function resourceName(uri) {
53
66
  url = new URL(uri);
54
67
  }
55
68
  catch {
56
- return fail();
69
+ return protocolFailure(uri);
57
70
  }
58
71
  const segment = url.pathname.split("/").filter(Boolean).at(-1);
59
72
  if (segment === undefined)
60
- return fail();
73
+ return protocolFailure(uri);
61
74
  try {
62
- return nonBlank(decodeURIComponent(segment));
75
+ const decoded = decodeURIComponent(segment);
76
+ if (decoded.trim() === "")
77
+ return protocolFailure(uri);
78
+ return decoded;
63
79
  }
64
80
  catch {
65
- return fail();
81
+ return protocolFailure(uri);
66
82
  }
67
83
  }
68
84
  function normalizeContent(value) {
69
85
  if (!isRecord(value))
70
- return fail();
86
+ return protocolFailure(value);
71
87
  if (value.type === "text") {
72
- return { type: "text", text: nonBlank(value.text) };
88
+ if (typeof value.text !== "string" || value.text.trim() === "") {
89
+ return protocolFailure(value);
90
+ }
91
+ return { type: "text", text: value.text };
92
+ }
93
+ if (value.type !== "resource" || !isRecord(value.resource)) {
94
+ return protocolFailure(value);
73
95
  }
74
- if (value.type !== "resource" || !isRecord(value.resource))
75
- return fail();
76
96
  if ("blob" in value.resource || "text" in value.resource)
77
- return fail();
78
- const uri = nonBlank(value.resource.uri);
97
+ return protocolFailure(value);
98
+ if (typeof value.resource.uri !== "string" || value.resource.uri.trim() === "") {
99
+ return protocolFailure(value);
100
+ }
101
+ const uri = value.resource.uri;
79
102
  const mimeType = value.resource.mimeType;
80
103
  if (mimeType !== undefined && (typeof mimeType !== "string" || mimeType.trim() === "")) {
81
- return fail();
104
+ return protocolFailure(value);
82
105
  }
83
106
  return {
84
107
  type: "resource_link",
@@ -87,23 +110,102 @@ function normalizeContent(value) {
87
110
  ...(mimeType === undefined ? {} : { mimeType })
88
111
  };
89
112
  }
113
+ export class ToolTaskFailedError extends Error {
114
+ remoteCode;
115
+ remoteBody;
116
+ constructor(evidence = {}) {
117
+ super("稿定工具任务执行失败。");
118
+ this.name = "ToolTaskFailedError";
119
+ this.remoteCode = evidence.remoteCode;
120
+ this.remoteBody = evidence.remoteBody;
121
+ }
122
+ }
123
+ function failedPoll(error, difyTaskId) {
124
+ return {
125
+ state: "failed",
126
+ error,
127
+ ...(difyTaskId === undefined ? {} : { difyTaskId })
128
+ };
129
+ }
130
+ function optionalInnerDifyTaskId(value) {
131
+ if (typeof value !== "string" || value.trim() === "")
132
+ return undefined;
133
+ try {
134
+ const parsed = JSON.parse(value);
135
+ if (!isRecord(parsed))
136
+ return undefined;
137
+ const difyTaskId = parsed.dify_task_id;
138
+ return typeof difyTaskId === "string" && difyTaskId.trim() !== ""
139
+ ? difyTaskId
140
+ : undefined;
141
+ }
142
+ catch {
143
+ return undefined;
144
+ }
145
+ }
90
146
  function pollResult(value, taskId) {
91
147
  if (!Array.isArray(value))
92
- return fail();
148
+ return protocolFailure(value);
93
149
  const item = value.find((candidate) => isRecord(candidate) && candidate.task_id === taskId);
94
- if (!isRecord(item) || item.code !== "200")
95
- return fail();
96
- const inner = parseRecord(nonBlank(item.result));
97
- if (!isRecord(inner.result) || inner.result.isError !== false)
98
- return fail();
150
+ if (!isRecord(item))
151
+ return { state: "pending" };
152
+ if (typeof item.code !== "string" || item.code.trim() === "") {
153
+ return protocolFailure(item);
154
+ }
155
+ if (item.code !== "200") {
156
+ return failedPoll(new ToolTaskFailedError(remoteErrorEvidence(item)), optionalInnerDifyTaskId(item.result));
157
+ }
158
+ if (typeof item.result !== "string" || item.result.trim() === "") {
159
+ return protocolFailure(item);
160
+ }
161
+ let inner;
162
+ try {
163
+ const parsed = JSON.parse(item.result);
164
+ if (!isRecord(parsed))
165
+ return protocolFailure(item);
166
+ inner = parsed;
167
+ }
168
+ catch (error) {
169
+ if (error instanceof RemoteProtocolError)
170
+ throw error;
171
+ return protocolFailure(item);
172
+ }
173
+ const difyTaskId = inner.dify_task_id;
174
+ if (difyTaskId !== undefined
175
+ && (typeof difyTaskId !== "string" || difyTaskId.trim() === "")) {
176
+ return protocolFailure(inner);
177
+ }
178
+ if (!isRecord(inner.result)) {
179
+ return failedPoll(new RemoteProtocolError(remoteErrorEvidence(inner)), difyTaskId);
180
+ }
181
+ if (inner.result.isError === true) {
182
+ return failedPoll(new ToolTaskFailedError(remoteErrorEvidence(inner)), difyTaskId);
183
+ }
184
+ if (inner.result.isError !== false) {
185
+ return failedPoll(new RemoteProtocolError(remoteErrorEvidence(inner)), difyTaskId);
186
+ }
99
187
  const content = inner.result.content;
100
- if (content === undefined)
101
- return undefined;
102
- if (!Array.isArray(content))
103
- return fail();
104
- if (content.length === 0)
105
- return undefined;
106
- return content.map(normalizeContent);
188
+ if (content === undefined || (Array.isArray(content) && content.length === 0)) {
189
+ return {
190
+ state: "pending",
191
+ ...(difyTaskId === undefined ? {} : { difyTaskId })
192
+ };
193
+ }
194
+ if (!Array.isArray(content)) {
195
+ return failedPoll(new RemoteProtocolError(remoteErrorEvidence(inner)), difyTaskId);
196
+ }
197
+ try {
198
+ return {
199
+ state: "complete",
200
+ content: content.map(normalizeContent),
201
+ ...(difyTaskId === undefined ? {} : { difyTaskId })
202
+ };
203
+ }
204
+ catch (error) {
205
+ if (error instanceof RemoteProtocolError)
206
+ return failedPoll(error, difyTaskId);
207
+ throw error;
208
+ }
107
209
  }
108
210
  async function safePost(transport, request) {
109
211
  try {
@@ -112,7 +214,7 @@ async function safePost(transport, request) {
112
214
  catch (error) {
113
215
  if (request.signal.aborted)
114
216
  throw request.signal.reason;
115
- if (error instanceof RemoteRequestError)
217
+ if (error instanceof RemoteRequestError || error instanceof RemoteProtocolError)
116
218
  throw error;
117
219
  throw new RemoteRequestError();
118
220
  }
@@ -150,21 +252,25 @@ export function createToolApiAdapter(dependencies) {
150
252
  signal
151
253
  })));
152
254
  signal.throwIfAborted();
153
- const taskId = await dependencies.telemetry.stage("submit", async () => submittedTask(await safePost(dependencies.transport, {
154
- path: "/gdesign/tool/v1/dify/call_async",
155
- body: {
156
- jsonrpc: "2.0",
157
- id: 0,
158
- method: "tools/call",
159
- params: {
160
- name: intent.name,
161
- arguments: { ...intent.arguments, content_id: managedContentId }
162
- }
163
- },
164
- credential: access.credential,
165
- organizationId: access.organizationId,
166
- signal
167
- })));
255
+ const taskId = await dependencies.telemetry.stage("submit", async () => {
256
+ dependencies.telemetry.annotate({ content_id: managedContentId });
257
+ return submittedTask(await safePost(dependencies.transport, {
258
+ path: "/gdesign/tool/v1/dify/call_async",
259
+ body: {
260
+ jsonrpc: "2.0",
261
+ id: 0,
262
+ method: "tools/call",
263
+ params: {
264
+ name: intent.name,
265
+ arguments: { ...intent.arguments, content_id: managedContentId }
266
+ }
267
+ },
268
+ credential: access.credential,
269
+ organizationId: access.organizationId,
270
+ signal
271
+ }));
272
+ });
273
+ dependencies.telemetry.annotate({ task_id: taskId });
168
274
  return dependencies.telemetry.stage("wait", async () => {
169
275
  let transientFailures = 0;
170
276
  while (true) {
@@ -191,9 +297,14 @@ export function createToolApiAdapter(dependencies) {
191
297
  await sleep(transientFailures * 1_000, signal);
192
298
  continue;
193
299
  }
194
- const result = pollResult(response, taskId);
195
- if (result !== undefined)
196
- return result;
300
+ const poll = pollResult(response, taskId);
301
+ if (poll.difyTaskId !== undefined) {
302
+ dependencies.telemetry.annotate({ dify_task_id: poll.difyTaskId });
303
+ }
304
+ if (poll.state === "failed")
305
+ throw poll.error;
306
+ if (poll.state === "complete")
307
+ return poll.content;
197
308
  await sleep(2_000, signal);
198
309
  }
199
310
  });
@@ -1,6 +1,6 @@
1
1
  import { parseSafeRemoteAssetUrl, UrlSafetyError } from "../../platform/url-safety.js";
2
- import { findModel, findToolModel, projectModelDetail, projectModelList, projectToolList, ToolInputError } from "./catalog.js";
3
- import { assertModelArguments, buildModelArgumentsSchema, buildToolArgumentsSchema } from "./dynamic-schema.js";
2
+ import { findToolModel, projectModelDetail, projectModelList, projectToolList, ToolInputError } from "./catalog.js";
3
+ import { assertModelArguments, buildToolArgumentsSchema } from "./dynamic-schema.js";
4
4
  const mediaTypes = {
5
5
  avif: "image/avif",
6
6
  gif: "image/gif",
@@ -67,9 +67,6 @@ export function createToolUseCases(dependencies) {
67
67
  async getArgumentsSchema({ name, access, signal }) {
68
68
  return buildToolArgumentsSchema(await load(access, signal), name);
69
69
  },
70
- async getModelArgumentsSchema({ model, access, signal }) {
71
- return buildModelArgumentsSchema(findModel(await load(access, signal), model));
72
- },
73
70
  async call({ input, access, signal }) {
74
71
  const catalog = await load(access, signal);
75
72
  const model = await dependencies.telemetry.stage("input", () => {
@@ -0,0 +1,66 @@
1
+ import { redact, redactSensitiveText } from "./redact.js";
2
+ const maximumLength = 8_192;
3
+ const truncationMarker = "[TRUNCATED]";
4
+ export function remoteErrorEvidence(value) {
5
+ const normalized = parseJson(value);
6
+ const remoteCode = topLevelCode(normalized);
7
+ const sanitized = sanitize(normalized);
8
+ if (sanitized === undefined) {
9
+ return remoteCode === undefined ? {} : { remoteCode };
10
+ }
11
+ const remoteBody = sanitized.length <= maximumLength
12
+ ? sanitized
13
+ : `${sanitized.slice(0, maximumLength - truncationMarker.length)}${truncationMarker}`;
14
+ return {
15
+ ...(remoteCode === undefined ? {} : { remoteCode }),
16
+ remoteBody
17
+ };
18
+ }
19
+ function parseJson(value) {
20
+ if (typeof value !== "string")
21
+ return value;
22
+ try {
23
+ return JSON.parse(value);
24
+ }
25
+ catch {
26
+ return value;
27
+ }
28
+ }
29
+ function topLevelCode(value) {
30
+ if (value === null || typeof value !== "object" || Array.isArray(value))
31
+ return undefined;
32
+ if (!Object.prototype.hasOwnProperty.call(value, "code"))
33
+ return undefined;
34
+ const code = value.code;
35
+ if (typeof code === "string")
36
+ return redactRemoteText(code);
37
+ return typeof code === "number" ? String(code) : undefined;
38
+ }
39
+ function sanitize(value) {
40
+ if (typeof value === "string")
41
+ return redactRemoteText(value);
42
+ try {
43
+ return JSON.stringify(redactCookieAssignments(redact(value)));
44
+ }
45
+ catch {
46
+ return undefined;
47
+ }
48
+ }
49
+ function redactRemoteText(value) {
50
+ return redactCookieAssignment(redactSensitiveText(value));
51
+ }
52
+ function redactCookieAssignments(value) {
53
+ if (typeof value === "string")
54
+ return redactCookieAssignment(value);
55
+ if (value === null || typeof value !== "object")
56
+ return value;
57
+ if (Array.isArray(value))
58
+ return value.map(redactCookieAssignments);
59
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
60
+ key,
61
+ redactCookieAssignments(item)
62
+ ]));
63
+ }
64
+ function redactCookieAssignment(value) {
65
+ return value.replace(/\b(cookie)\s*=\s*[^\s,;}&]+/giu, "$1=[REDACTED]");
66
+ }
@@ -1,6 +1,10 @@
1
1
  export class RemoteProtocolError extends Error {
2
- constructor() {
2
+ remoteCode;
3
+ remoteBody;
4
+ constructor(evidence = {}) {
3
5
  super("稿定服务响应与当前 CLI 不兼容。");
4
6
  this.name = "RemoteProtocolError";
7
+ this.remoteCode = evidence.remoteCode;
8
+ this.remoteBody = evidence.remoteBody;
5
9
  }
6
10
  }
@@ -1,12 +1,18 @@
1
+ import { remoteErrorEvidence } from "./remote-error-evidence.js";
2
+ import { RemoteProtocolError } from "./remote-protocol.js";
1
3
  import { canonicalizeQuery, signRequest } from "./signature.js";
2
4
  export class RemoteRequestError extends Error {
3
5
  status;
4
6
  transient;
5
- constructor(status, transient = false) {
7
+ remoteCode;
8
+ remoteBody;
9
+ constructor(status, transient = false, evidence = {}) {
6
10
  super(status === undefined ? "无法连接稿定服务。" : `稿定服务请求失败(HTTP ${status})。`);
7
11
  this.name = "RemoteRequestError";
8
12
  this.status = status;
9
13
  this.transient = transient;
14
+ this.remoteCode = evidence.remoteCode;
15
+ this.remoteBody = evidence.remoteBody;
10
16
  }
11
17
  }
12
18
  const TRANSIENT_NETWORK_CODES = new Set([
@@ -56,6 +62,8 @@ export function createSignedHttpTransport(options) {
56
62
  }),
57
63
  "X-Timestamp": String(timestamp)
58
64
  };
65
+ if (options.traceparent !== undefined)
66
+ headers.traceparent = options.traceparent();
59
67
  if (options.channelId !== undefined)
60
68
  headers["X-Channel-Id"] = options.channelId;
61
69
  if (body !== undefined)
@@ -83,14 +91,22 @@ export function createSignedHttpTransport(options) {
83
91
  }
84
92
  }
85
93
  async function json(response) {
86
- if (!response.ok)
87
- throw new RemoteRequestError(response.status);
94
+ let body;
88
95
  try {
89
- return await response.json();
96
+ body = await response.text();
90
97
  }
91
98
  catch {
92
99
  throw new RemoteRequestError(response.status);
93
100
  }
101
+ const evidence = remoteErrorEvidence(body);
102
+ if (!response.ok)
103
+ throw new RemoteRequestError(response.status, false, evidence);
104
+ try {
105
+ return JSON.parse(body);
106
+ }
107
+ catch {
108
+ throw new RemoteProtocolError(evidence);
109
+ }
94
110
  }
95
111
  return {
96
112
  async getJson(request) {
@@ -101,7 +117,17 @@ export function createSignedHttpTransport(options) {
101
117
  },
102
118
  async postStream(request) {
103
119
  const response = await send("POST", request, request.accept, JSON.stringify(request.body), false);
104
- if (!response.ok || !response.body)
120
+ if (!response.ok) {
121
+ let body;
122
+ try {
123
+ body = await response.text();
124
+ }
125
+ catch {
126
+ throw new RemoteRequestError(response.status);
127
+ }
128
+ throw new RemoteRequestError(response.status, false, remoteErrorEvidence(body));
129
+ }
130
+ if (!response.body)
105
131
  throw new RemoteRequestError(response.status);
106
132
  return response.body;
107
133
  }
@@ -7,16 +7,19 @@ const detailedStages = {
7
7
  update: new Set(["registry", "install", "sync", "verify"])
8
8
  };
9
9
  export function createTelemetryInvocation(options) {
10
- const invocationId = options.idFactory();
10
+ const traceId = options.traceIdFactory();
11
11
  const startedAt = options.now();
12
12
  let definition;
13
13
  const context = {};
14
14
  const stages = [];
15
15
  return {
16
- invocationId,
16
+ traceId,
17
17
  get selected() {
18
18
  return definition !== undefined;
19
19
  },
20
+ traceparent() {
21
+ return `00-${traceId}-${options.spanIdFactory()}-${traceFlags(definition)}`;
22
+ },
20
23
  select(nextDefinition) {
21
24
  definition = nextDefinition;
22
25
  },
@@ -30,6 +33,13 @@ export function createTelemetryInvocation(options) {
30
33
  context.tool_name = annotation.tool_name;
31
34
  if (annotation.model_id !== undefined)
32
35
  context.model_id = annotation.model_id;
36
+ if (annotation.content_id !== undefined)
37
+ context.content_id = annotation.content_id;
38
+ if (annotation.task_id !== undefined)
39
+ context.task_id = annotation.task_id;
40
+ if (annotation.dify_task_id !== undefined) {
41
+ context.dify_task_id = annotation.dify_task_id;
42
+ }
33
43
  if (annotation.parameters !== undefined) {
34
44
  context.parameters = { ...annotation.parameters };
35
45
  }
@@ -72,9 +82,9 @@ export function createTelemetryInvocation(options) {
72
82
  ? stages
73
83
  .filter((stage) => allowedStages.has(stage.name))
74
84
  .map((stage) => ({
75
- schema_version: "1",
85
+ schema_version: "2",
76
86
  event_name: "stage_finished",
77
- invocation_id: invocationId,
87
+ trace_id: traceId,
78
88
  operation: selectedDefinition.operation,
79
89
  stage: stage.name,
80
90
  outcome: stage.outcome,
@@ -84,9 +94,9 @@ export function createTelemetryInvocation(options) {
84
94
  }))
85
95
  : [];
86
96
  const command = {
87
- schema_version: "1",
97
+ schema_version: "2",
88
98
  event_name: "command_finished",
89
- invocation_id: invocationId,
99
+ trace_id: traceId,
90
100
  operation: selectedDefinition.operation,
91
101
  ...(mode === undefined ? {} : { mode }),
92
102
  outcome: completion.outcome,
@@ -101,6 +111,11 @@ export function createTelemetryInvocation(options) {
101
111
  : { organization_id: context.organization_id }),
102
112
  ...(context.tool_name === undefined ? {} : { tool_name: context.tool_name }),
103
113
  ...(context.model_id === undefined ? {} : { model_id: context.model_id }),
114
+ ...(context.content_id === undefined ? {} : { content_id: context.content_id }),
115
+ ...(context.task_id === undefined ? {} : { task_id: context.task_id }),
116
+ ...(context.dify_task_id === undefined
117
+ ? {}
118
+ : { dify_task_id: context.dify_task_id }),
104
119
  ...(context.parameters === undefined
105
120
  ? {}
106
121
  : { parameters: { ...context.parameters } }),
@@ -112,7 +127,13 @@ export function createTelemetryInvocation(options) {
112
127
  ?? "unknown",
113
128
  ...(completion.httpStatus === undefined
114
129
  ? {}
115
- : { http_status: completion.httpStatus })
130
+ : { http_status: completion.httpStatus }),
131
+ ...(completion.remoteErrorCode === undefined
132
+ ? {}
133
+ : { remote_error_code: completion.remoteErrorCode }),
134
+ ...(completion.remoteErrorBody === undefined
135
+ ? {}
136
+ : { remote_error_body: completion.remoteErrorBody })
116
137
  }
117
138
  : {})
118
139
  };
@@ -120,6 +141,13 @@ export function createTelemetryInvocation(options) {
120
141
  }
121
142
  };
122
143
  }
144
+ function traceFlags(definition) {
145
+ if (definition === undefined)
146
+ return "00";
147
+ const paidExecution = (definition.operation === "agent.send"
148
+ || definition.operation === "tool.call") && definition.mode?.() === "execute";
149
+ return paidExecution ? "01" : "00";
150
+ }
123
151
  function stageKey(operation, mode) {
124
152
  return mode === undefined ? operation : `${operation}:${mode}`;
125
153
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gaoding-cli",
3
- "version": "1.0.0-alpha.14",
3
+ "version": "1.0.0-alpha.15",
4
4
  "description": "Gaoding command-line interface for agents and people.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -3,7 +3,7 @@
3
3
  ## 选择入口
4
4
 
5
5
  - 目标开放、需要稿定 Agent 完成创作时,使用 `gd-cli agent send`。
6
- - 已知要调用具体图片、视频或文本能力时,依次发现 Tool、选择 Model、读取输入 Schema,再调用 Tool。
6
+ - 已知要调用具体图片、视频或文本能力时,依次发现 Tool、选择 Model、读取 Model detail,再调用 Tool。
7
7
  - 不猜 Tool 名、Model 名或参数;以当前命令输出为准。
8
8
 
9
9
  ## Agent 创作
@@ -25,16 +25,25 @@ gd-cli agent send --input request.json
25
25
  gd-cli tool list
26
26
  gd-cli model list --tool <tool>
27
27
  gd-cli model get <model>
28
- gd-cli model get <model> --schema
29
28
  gd-cli tool call <tool> --input request.json
30
29
  ```
31
30
 
32
- `tool list` 给出可用 Tool;`model list` 和 `model get` 给出当前可用 Model、说明、费用与预估耗时;`model get --schema` 给出所选 Model 的实时运行时输入 Schema。不要把列表值固化在提示或脚本中。
31
+ `tool list` 给出可用 Tool;`model list` 和 `model get` 给出当前可用 Model、说明、费用、预估耗时和结构化参数。`model get` `parameters` 是所选 Model 的实际调用元数据:遵循 `required`、`type`、`description`,SELECT 参数使用 `options[].value`,required 参数带 `default` 时在用户未指定时显式传入默认值。`usageDescription` 描述跨字段规则,例如视频 mode 对首尾帧或素材字段的要求;这类组合不能只从单个字段推断。不要把列表值固化在提示或脚本中。
33
32
 
34
- 按所选 Model Schema 构造 JSON:提供全部 required 字段;SELECT 参数使用 `options[].value`;required 字段带 default 且用户未指定时,在输入中显式使用该 default。存在 width、height、resolution 等专用字段时,将用户要求写入对应结构化参数,不要只写在 prompt 中。`tool call <tool> --schema` 返回整个 Tool 下所有 Model 的参数并集,只用于 Tool 级发现,不替代选定 Model Schema。
33
+ 按所选 Model detail 构造 JSON:提供全部 required 字段;SELECT 参数使用 `options[].value`;required 字段带 default 且用户未指定时,在输入中显式使用该 default。存在 width、height、resolution 等专用字段时,将用户要求写入对应结构化参数,不要只写在 prompt 中。`tool call <tool> --schema` 返回整个 Tool 下所有 Model 的参数并集,只用于 Tool 级发现;选定 Model 后以 `model get` 的参数和 `usageDescription` 为准。
34
+
35
+ 务必将 `model get <model>` 返回的 `model` 原样放在请求 JSON 的顶层 `model` 字段(即运行时的 `arguments.model`),与所选 Model 的参数字段并列;不要只复制 `parameters`。例如:
36
+
37
+ ```json
38
+ {
39
+ "model": "<model>",
40
+ "prompt": "<required prompt>",
41
+ "<parameter>": "<value>"
42
+ }
43
+ ```
35
44
 
36
45
  成功结果包含 `content` 和 `usage`。`content` 是文本或资源链接;`usage` 包含所选模型及模型目录公布的稿豆价格区间,不表示实际扣费。
37
46
 
38
47
  ## 本地媒体
39
48
 
40
- 按所选 Schema 在输入中提交本地媒体引用。CLI 会在内部通过 DAM 完成临时上传并把公网 URL 传给创作服务;不要自行伪造 URL,也不要读取用户未明确授权的路径。
49
+ 按所选 Model detail 在输入中提交本地媒体引用。CLI 会在内部通过 DAM 完成临时上传并把公网 URL 传给创作服务;不要自行伪造 URL,也不要读取用户未明确授权的路径。