gaoding-cli 1.0.0-alpha.14 → 1.0.0-alpha.16

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();
@@ -42,10 +47,33 @@ function parseIntent(value) {
42
47
  arguments: parseRecord(nonBlank(value.arguments))
43
48
  };
44
49
  }
50
+ function isEmptyOptionalMedia(value) {
51
+ return value === undefined
52
+ || value === null
53
+ || value === ""
54
+ || (Array.isArray(value) && value.length === 0)
55
+ || (typeof value === "string" && value.trim() === "[]");
56
+ }
57
+ function withoutEmptyOptionalMedia(value, optionalMediaParameters) {
58
+ const normalized = { ...value };
59
+ for (const name of optionalMediaParameters) {
60
+ if (isEmptyOptionalMedia(normalized[name]))
61
+ delete normalized[name];
62
+ }
63
+ return normalized;
64
+ }
45
65
  function submittedTask(value) {
46
- if (!isRecord(value) || !isRecord(value.result) || value.result.isError !== false)
47
- return fail();
48
- return nonBlank(value.task_id);
66
+ if (!isRecord(value) || !isRecord(value.result))
67
+ return protocolFailure(value);
68
+ if (value.result.isError === true) {
69
+ throw new ToolTaskFailedError(remoteErrorEvidence(value));
70
+ }
71
+ if (value.result.isError !== false)
72
+ return protocolFailure(value);
73
+ if (typeof value.task_id !== "string" || value.task_id.trim() === "") {
74
+ return protocolFailure(value);
75
+ }
76
+ return value.task_id;
49
77
  }
50
78
  function resourceName(uri) {
51
79
  let url;
@@ -53,32 +81,42 @@ function resourceName(uri) {
53
81
  url = new URL(uri);
54
82
  }
55
83
  catch {
56
- return fail();
84
+ return protocolFailure(uri);
57
85
  }
58
86
  const segment = url.pathname.split("/").filter(Boolean).at(-1);
59
87
  if (segment === undefined)
60
- return fail();
88
+ return protocolFailure(uri);
61
89
  try {
62
- return nonBlank(decodeURIComponent(segment));
90
+ const decoded = decodeURIComponent(segment);
91
+ if (decoded.trim() === "")
92
+ return protocolFailure(uri);
93
+ return decoded;
63
94
  }
64
95
  catch {
65
- return fail();
96
+ return protocolFailure(uri);
66
97
  }
67
98
  }
68
99
  function normalizeContent(value) {
69
100
  if (!isRecord(value))
70
- return fail();
101
+ return protocolFailure(value);
71
102
  if (value.type === "text") {
72
- return { type: "text", text: nonBlank(value.text) };
103
+ if (typeof value.text !== "string" || value.text.trim() === "") {
104
+ return protocolFailure(value);
105
+ }
106
+ return { type: "text", text: value.text };
107
+ }
108
+ if (value.type !== "resource" || !isRecord(value.resource)) {
109
+ return protocolFailure(value);
73
110
  }
74
- if (value.type !== "resource" || !isRecord(value.resource))
75
- return fail();
76
111
  if ("blob" in value.resource || "text" in value.resource)
77
- return fail();
78
- const uri = nonBlank(value.resource.uri);
112
+ return protocolFailure(value);
113
+ if (typeof value.resource.uri !== "string" || value.resource.uri.trim() === "") {
114
+ return protocolFailure(value);
115
+ }
116
+ const uri = value.resource.uri;
79
117
  const mimeType = value.resource.mimeType;
80
118
  if (mimeType !== undefined && (typeof mimeType !== "string" || mimeType.trim() === "")) {
81
- return fail();
119
+ return protocolFailure(value);
82
120
  }
83
121
  return {
84
122
  type: "resource_link",
@@ -87,23 +125,118 @@ function normalizeContent(value) {
87
125
  ...(mimeType === undefined ? {} : { mimeType })
88
126
  };
89
127
  }
128
+ export class ToolTaskFailedError extends Error {
129
+ remoteCode;
130
+ remoteBody;
131
+ constructor(evidence = {}) {
132
+ super("稿定工具任务执行失败。");
133
+ this.name = "ToolTaskFailedError";
134
+ this.remoteCode = evidence.remoteCode;
135
+ this.remoteBody = evidence.remoteBody;
136
+ }
137
+ }
138
+ function failedPoll(error, difyTaskId) {
139
+ return {
140
+ state: "failed",
141
+ error,
142
+ ...(difyTaskId === undefined ? {} : { difyTaskId })
143
+ };
144
+ }
145
+ function optionalInnerDifyTaskId(value) {
146
+ if (typeof value !== "string" || value.trim() === "")
147
+ return undefined;
148
+ try {
149
+ const parsed = JSON.parse(value);
150
+ if (!isRecord(parsed))
151
+ return undefined;
152
+ const difyTaskId = parsed.dify_task_id;
153
+ return typeof difyTaskId === "string" && difyTaskId.trim() !== ""
154
+ ? difyTaskId
155
+ : undefined;
156
+ }
157
+ catch {
158
+ return undefined;
159
+ }
160
+ }
161
+ function isJsonRpcError(value) {
162
+ if (value.jsonrpc !== "2.0"
163
+ || Object.prototype.hasOwnProperty.call(value, "result")
164
+ || !isRecord(value.error)) {
165
+ return false;
166
+ }
167
+ return typeof value.error.code === "number"
168
+ && Number.isInteger(value.error.code)
169
+ && typeof value.error.message === "string"
170
+ && value.error.message.trim() !== "";
171
+ }
90
172
  function pollResult(value, taskId) {
91
173
  if (!Array.isArray(value))
92
- return fail();
174
+ return protocolFailure(value);
93
175
  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();
176
+ if (!isRecord(item))
177
+ return { state: "pending" };
178
+ if (typeof item.code !== "string" || item.code.trim() === "") {
179
+ return protocolFailure(item);
180
+ }
181
+ if (item.code !== "200") {
182
+ return failedPoll(new ToolTaskFailedError(remoteErrorEvidence(item)), optionalInnerDifyTaskId(item.result));
183
+ }
184
+ if (typeof item.result !== "string" || item.result.trim() === "") {
185
+ return protocolFailure(item);
186
+ }
187
+ let inner;
188
+ try {
189
+ const parsed = JSON.parse(item.result);
190
+ if (!isRecord(parsed))
191
+ return protocolFailure(item);
192
+ inner = parsed;
193
+ }
194
+ catch (error) {
195
+ if (error instanceof RemoteProtocolError)
196
+ throw error;
197
+ return protocolFailure(item);
198
+ }
199
+ const difyTaskId = inner.dify_task_id;
200
+ if (difyTaskId !== undefined
201
+ && (typeof difyTaskId !== "string" || difyTaskId.trim() === "")) {
202
+ return protocolFailure(inner);
203
+ }
204
+ if (Object.prototype.hasOwnProperty.call(inner, "error")) {
205
+ return isJsonRpcError(inner)
206
+ ? failedPoll(new ToolTaskFailedError(remoteErrorEvidence(inner)), difyTaskId)
207
+ : failedPoll(new RemoteProtocolError(remoteErrorEvidence(inner)), difyTaskId);
208
+ }
209
+ if (!isRecord(inner.result)) {
210
+ return failedPoll(new RemoteProtocolError(remoteErrorEvidence(inner)), difyTaskId);
211
+ }
212
+ if (inner.result.isError === true) {
213
+ return failedPoll(new ToolTaskFailedError(remoteErrorEvidence(inner)), difyTaskId);
214
+ }
215
+ if (inner.result.isError !== false) {
216
+ return failedPoll(new RemoteProtocolError(remoteErrorEvidence(inner)), difyTaskId);
217
+ }
99
218
  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);
219
+ if (content === undefined || (Array.isArray(content) && content.length === 0)) {
220
+ return {
221
+ state: "pending",
222
+ ...(difyTaskId === undefined ? {} : { difyTaskId })
223
+ };
224
+ }
225
+ if (!Array.isArray(content)) {
226
+ return failedPoll(new RemoteProtocolError(remoteErrorEvidence(inner)), difyTaskId);
227
+ }
228
+ try {
229
+ return {
230
+ state: "complete",
231
+ content: content.map(normalizeContent),
232
+ ...(difyTaskId === undefined ? {} : { difyTaskId })
233
+ };
234
+ }
235
+ catch (error) {
236
+ if (error instanceof RemoteProtocolError)
237
+ return failedPoll(error, difyTaskId);
238
+ throw error;
239
+ }
107
240
  }
108
241
  async function safePost(transport, request) {
109
242
  try {
@@ -112,7 +245,7 @@ async function safePost(transport, request) {
112
245
  catch (error) {
113
246
  if (request.signal.aborted)
114
247
  throw request.signal.reason;
115
- if (error instanceof RemoteRequestError)
248
+ if (error instanceof RemoteRequestError || error instanceof RemoteProtocolError)
116
249
  throw error;
117
250
  throw new RemoteRequestError();
118
251
  }
@@ -138,33 +271,45 @@ export function createToolApiAdapter(dependencies) {
138
271
  async execute({ invocation, access, signal }) {
139
272
  signal.throwIfAborted();
140
273
  const managedContentId = contentId(createContentId());
141
- const intent = await dependencies.telemetry.stage("intent", async () => parseIntent(await safePost(dependencies.transport, {
142
- path: "/ai-agent/v1/tool-intent",
143
- body: {
144
- prompt: invocation.prompt,
145
- scene_code: invocation.model,
146
- parameters: invocation.parameters
147
- },
148
- credential: access.credential,
149
- organizationId: access.organizationId,
150
- signal
151
- })));
274
+ const resolved = invocation.kind === "direct"
275
+ ? { name: invocation.name, arguments: invocation.arguments }
276
+ : await dependencies.telemetry.stage("intent", async () => {
277
+ const intent = parseIntent(await safePost(dependencies.transport, {
278
+ path: "/ai-agent/v1/tool-intent",
279
+ body: {
280
+ prompt: invocation.prompt,
281
+ scene_code: invocation.model,
282
+ parameters: invocation.parameters
283
+ },
284
+ credential: access.credential,
285
+ organizationId: access.organizationId,
286
+ signal
287
+ }));
288
+ return {
289
+ ...intent,
290
+ arguments: withoutEmptyOptionalMedia(intent.arguments, invocation.optionalMediaParameters)
291
+ };
292
+ });
152
293
  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
- })));
294
+ const taskId = await dependencies.telemetry.stage("submit", async () => {
295
+ dependencies.telemetry.annotate({ content_id: managedContentId });
296
+ return submittedTask(await safePost(dependencies.transport, {
297
+ path: "/gdesign/tool/v1/dify/call_async",
298
+ body: {
299
+ jsonrpc: "2.0",
300
+ id: 0,
301
+ method: "tools/call",
302
+ params: {
303
+ name: resolved.name,
304
+ arguments: { ...resolved.arguments, content_id: managedContentId }
305
+ }
306
+ },
307
+ credential: access.credential,
308
+ organizationId: access.organizationId,
309
+ signal
310
+ }));
311
+ });
312
+ dependencies.telemetry.annotate({ task_id: taskId });
168
313
  return dependencies.telemetry.stage("wait", async () => {
169
314
  let transientFailures = 0;
170
315
  while (true) {
@@ -191,9 +336,14 @@ export function createToolApiAdapter(dependencies) {
191
336
  await sleep(transientFailures * 1_000, signal);
192
337
  continue;
193
338
  }
194
- const result = pollResult(response, taskId);
195
- if (result !== undefined)
196
- return result;
339
+ const poll = pollResult(response, taskId);
340
+ if (poll.difyTaskId !== undefined) {
341
+ dependencies.telemetry.annotate({ dify_task_id: poll.difyTaskId });
342
+ }
343
+ if (poll.state === "failed")
344
+ throw poll.error;
345
+ if (poll.state === "complete")
346
+ return poll.content;
197
347
  await sleep(2_000, signal);
198
348
  }
199
349
  });
@@ -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", () => {
@@ -89,6 +86,7 @@ export function createToolUseCases(dependencies) {
89
86
  });
90
87
  const prompt = [];
91
88
  const requestParameters = {};
89
+ const directArguments = {};
92
90
  const prepare = async () => {
93
91
  for (const parameter of model.parameters) {
94
92
  signal.throwIfAborted();
@@ -98,14 +96,17 @@ export function createToolUseCases(dependencies) {
98
96
  if (parameter.type === "string") {
99
97
  if (parameter.name === "prompt") {
100
98
  prompt.push({ type: "text", content: value });
99
+ directArguments[parameter.name] = value;
101
100
  }
102
101
  else {
103
102
  requestParameters[parameter.name] = value;
103
+ directArguments[parameter.name] = value;
104
104
  }
105
105
  continue;
106
106
  }
107
107
  if (parameter.type === "number") {
108
108
  requestParameters[parameter.name] = value;
109
+ directArguments[parameter.name] = value;
109
110
  continue;
110
111
  }
111
112
  const values = parameter.type === "uri[]" ? value : [value];
@@ -141,8 +142,26 @@ export function createToolUseCases(dependencies) {
141
142
  await prepare();
142
143
  }
143
144
  signal.throwIfAborted();
145
+ const requiresIntent = prompt.some((item) => item.type === "media")
146
+ || Object.values(requestParameters).some((value) => value === "");
147
+ const invocation = requiresIntent
148
+ ? {
149
+ kind: "intent",
150
+ model: model.model,
151
+ prompt,
152
+ parameters: requestParameters,
153
+ optionalMediaParameters: model.parameters
154
+ .filter((parameter) => ((parameter.type === "uri" || parameter.type === "uri[]")
155
+ && !parameter.required))
156
+ .map((parameter) => parameter.name)
157
+ }
158
+ : {
159
+ kind: "direct",
160
+ name: model.model,
161
+ arguments: directArguments
162
+ };
144
163
  const content = await dependencies.execution.execute({
145
- invocation: { model: model.model, prompt, parameters: requestParameters },
164
+ invocation,
146
165
  access,
147
166
  signal
148
167
  });
@@ -0,0 +1,80 @@
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) ?? jsonRpcErrorCode(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
+ return normalizeCode(value.code);
35
+ }
36
+ function jsonRpcErrorCode(value) {
37
+ if (value === null || typeof value !== "object" || Array.isArray(value))
38
+ return undefined;
39
+ const record = value;
40
+ if (record.jsonrpc !== "2.0"
41
+ || record.error === null
42
+ || typeof record.error !== "object"
43
+ || Array.isArray(record.error)) {
44
+ return undefined;
45
+ }
46
+ return normalizeCode(record.error.code);
47
+ }
48
+ function normalizeCode(code) {
49
+ if (typeof code === "string")
50
+ return redactRemoteText(code);
51
+ return typeof code === "number" ? String(code) : undefined;
52
+ }
53
+ function sanitize(value) {
54
+ if (typeof value === "string")
55
+ return redactRemoteText(value);
56
+ try {
57
+ return JSON.stringify(redactCookieAssignments(redact(value)));
58
+ }
59
+ catch {
60
+ return undefined;
61
+ }
62
+ }
63
+ function redactRemoteText(value) {
64
+ return redactCookieAssignment(redactSensitiveText(value));
65
+ }
66
+ function redactCookieAssignments(value) {
67
+ if (typeof value === "string")
68
+ return redactCookieAssignment(value);
69
+ if (value === null || typeof value !== "object")
70
+ return value;
71
+ if (Array.isArray(value))
72
+ return value.map(redactCookieAssignments);
73
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
74
+ key,
75
+ redactCookieAssignments(item)
76
+ ]));
77
+ }
78
+ function redactCookieAssignment(value) {
79
+ return value.replace(/\b(cookie)\s*=\s*[^\s,;}&]+/giu, "$1=[REDACTED]");
80
+ }
@@ -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.16",
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,也不要读取用户未明确授权的路径。