Skip to content

Commit 6b70b99

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/aliyun-bl-integration
# Conflicts: # packages/core/src/console/gateway.ts
2 parents b83d593 + a7cb810 commit 6b70b99

16 files changed

Lines changed: 480 additions & 31 deletions

File tree

‎docs/agents/telemetry-change.md‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,19 @@
2727

2828
### 1. 三套鉴权与埋点标识
2929

30-
| 命令声明 | 凭证 / 请求域 | 主要请求出口 | 后端埋点标识 | 前端埋点标识(AEM) |
31-
| ----------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------ |
32-
| `auth: "apiKey"` | API Key;DashScope / OpenAI-compatible 模型域 | `Client.request/requestJson`、`McpClient`、Managed Agent instrumented fetch、上传策略 | 有:`User-Agent`、`x-dashscope-source-config`、`X-Dashscope-Service: bailian-cli` | 有:`pid=bailian-cli-node`、`authMethod=apiKey` |
33-
| `auth: "console"` | Console access token;Bailian Console Gateway | `callConsoleGateway()` → `/cli/api.json` | 有:Console Gateway 后端直接标识 | 有:`pid=bailian-cli-node`、`authMethod=console` |
34-
| `auth: "openapi"` | AccessKey ID/Secret,可选 STS token;阿里云 OpenAPI | `Client.openApiJson()` | 有:`x-dashscope-source-config`、`X-Dashscope-Service: bailian-cli` | 有:`pid=bailian-cli-node`、`authMethod=openapi` |
35-
| `auth: "none"` | 无凭证域 | 本地逻辑或命令自行管理的登录/配置流程 | 无 | 有:`pid=bailian-cli-node`、`authMethod=none` |
30+
| 命令声明 | 凭证 / 请求域 | 主要请求出口 | 后端埋点标识 | 前端埋点标识(AEM) |
31+
| ----------------- | --------------------------------------------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------ |
32+
| `auth: "apiKey"` | API Key;DashScope / OpenAI-compatible 模型域 | `Client.request/requestJson`、`McpClient`、Managed Agent / Advisor instrumented fetch、上传策略 | 有:`User-Agent`、`x-dashscope-source-config`、`X-Dashscope-Service: bailian-cli` | 有:`pid=bailian-cli-node`、`authMethod=apiKey` |
33+
| `auth: "console"` | Console access token;Bailian Console Gateway | `callConsoleGateway()` → `/cli/api.json` | 有:Console Gateway 后端直接标识 | 有:`pid=bailian-cli-node`、`authMethod=console` |
34+
| `auth: "openapi"` | AccessKey ID/Secret,可选 STS token;阿里云 OpenAPI | `Client.openApiJson()` | 有:`x-dashscope-source-config`、`X-Dashscope-Service: bailian-cli` | 有:`pid=bailian-cli-node`、`authMethod=openapi` |
35+
| `auth: "none"` | 无凭证域 | 本地逻辑或命令自行管理的登录/配置流程 | 无 | 有:`pid=bailian-cli-node`、`authMethod=none` |
3636

3737
`authMethod` 记录的是命令声明的鉴权域,不是凭证来源。它不会区分 API Key 来自 flag、env 还是 config。
3838
鉴权域是命令的准入门槛和主请求域,不保证命令内部只有一种网络出口;例如部分 `apiKey` 命令也可能读取匿名 Console 公共目录,Managed Agent 还可能访问其他 provider。
3939

4040
表中的后端埋点按该鉴权域的主要业务请求填写:
4141

42-
- Managed Agent 的 `User-Agent` 对所有 SDK 请求注入;`x-dashscope-source-config` 与 `X-Dashscope-Service` 仅对阿里云 host 注入
42+
- Managed Agent 与 Advisor 通过宿主注入的 `createInstrumentedFetch` 统一携带 `User-Agent`;渠道头仅对阿里云 host 注入。Advisor 的引擎保持独立,普通聊天、工具调用、SSE 与 JSON 重试共用该传输,`--verbose` 在 stderr 输出 endpoint、HTTP 状态与响应头中的 request_id,凭证脱敏且不输出请求体
4343
- DashScope 上传策略 `getPolicy` 走 `trackingHeaders()`(含 source-config 与 `X-Dashscope-Service`),没有显式 CLI `User-Agent`
4444
- Console Gateway 用户数据由后端直接区分,不依赖 `x-dashscope-source-config`;AEM 的 `authMethod=console` 是独立的命令侧统计
4545
- OpenAPI 的 ACS 签名头,以及 Console Gateway 的 `product`、`action`、`api` 是鉴权或路由字段,不计为埋点标识

‎packages/commands/src/commands/advisor/recommend.ts‎

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
agentLoop,
33
BailianError,
4+
createInstrumentedFetch,
45
createLLMClient,
56
createReadlineInteraction,
67
DEFAULT_LLM_MODEL,
@@ -137,8 +138,8 @@ const flags = {
137138
noThinking: {
138139
type: "switch",
139140
description: {
140-
"en-US": "Disable thinking mode for intent analysis (on by default)",
141-
"zh-CN": "关闭意图分析的思考模式(默认开启)",
141+
"en-US": "Deprecated no-op: thinking is always disabled for advisor LLM calls",
142+
"zh-CN": "已废弃且无效果:advisor 的 LLM 调用现已始终关闭思考模式",
142143
},
143144
},
144145
} as const;
@@ -226,11 +227,17 @@ export default defineCommand({
226227
llm: createLLMClient({
227228
baseUrl: ctx.client.baseUrl,
228229
apiKey: credential.token,
230+
fetch: createInstrumentedFetch({
231+
identity: ctx.identity,
232+
settings: ctx.settings,
233+
debug: (diagnostic) => ctx.output.debug(diagnostic),
234+
}),
229235
defaultModel: runtimeFlags.model ?? DEFAULT_LLM_MODEL,
230-
// Intent analysis is the only LLM call that decides the slate, so it
231-
// gets thinking mode by default (--no-thinking to disable). Other
232-
// stages stay in plain mode: thinking tokens are billed per call.
233-
thinkingStages: runtimeFlags.noThinking ? [] : ["intent"],
236+
// Disable thinking for every advisor LLM call. Intent analysis was the
237+
// only thinking stage, but it dominated latency without improving
238+
// clear recommendation requests. `--no-thinking` remains accepted as
239+
// a deprecated no-op for existing scripts.
240+
thinkingStages: [],
234241
}),
235242
catalog: () => loadCatalog(),
236243
families: () => loadFamilies(),

‎packages/commands/src/commands/finetune/capability.ts‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
TRAINING_TYPES_CLI,
1010
anonymousConsoleCall,
1111
UsageError,
12+
type HttpDeps,
1213
type Settings,
1314
type ModelCapability,
1415
type FlagsDef,
@@ -20,8 +21,11 @@ import {
2021
* records so capability fields (`supports` / `trainingTypes`) are preserved
2122
* for filtering.
2223
*/
23-
async function fetchAllFoundationModels(settings: Settings): Promise<ModelCapability[]> {
24-
const all = await fetchModelListAll(anonymousConsoleCall(settings));
24+
async function fetchAllFoundationModels(
25+
settings: Settings,
26+
diagnostics: Pick<HttpDeps, "debug">,
27+
): Promise<ModelCapability[]> {
28+
const all = await fetchModelListAll(anonymousConsoleCall(settings, diagnostics));
2529
return all as ModelCapability[];
2630
}
2731

@@ -85,6 +89,9 @@ export default defineCommand({
8589
},
8690
async run(ctx) {
8791
const { settings, flags } = ctx;
92+
const diagnostics: Pick<HttpDeps, "debug"> = {
93+
debug: (diagnostic) => ctx.output.debug(diagnostic),
94+
};
8895
const model = flags.baseModel || undefined;
8996
const trainingType = flags.trainingType || undefined;
9097

@@ -103,7 +110,7 @@ export default defineCommand({
103110

104111
// Direction 1: by model → which training types it supports.
105112
if (model) {
106-
const capability = await fetchModelCapability(settings, model);
113+
const capability = await fetchModelCapability(settings, model, diagnostics);
107114
if (!capability) {
108115
const resultData2 = { model, error: `No foundation model found matching "${model}".` };
109116
ctx.output.result(
@@ -136,7 +143,7 @@ export default defineCommand({
136143
);
137144
}
138145
const { method, variant } = trainingTypeMethodVariant(trainingType);
139-
const all = await fetchAllFoundationModels(settings);
146+
const all = await fetchAllFoundationModels(settings, diagnostics);
140147
const matched = all
141148
.filter((record) => modelSupportsTrainingType(record, trainingType))
142149
.map((record) => ({

‎packages/commands/src/commands/finetune/create.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
formatIssue,
1616
BailianError,
1717
ExitCode,
18+
type HttpDeps,
1819
type CommandContext,
1920
type CreateFineTuneRequest,
2021
type FineTuneHyperParameters,
@@ -562,6 +563,9 @@ async function runCreate<F extends FlagsDef>(
562563
ctx: CommandContext<F>,
563564
): Promise<void> {
564565
const { settings } = ctx;
566+
const diagnostics: Pick<HttpDeps, "debug"> = {
567+
debug: (diagnostic) => ctx.output.debug(diagnostic),
568+
};
565569
const flags = ctx.flags as Record<string, unknown>;
566570
const model = flags.baseModel as string;
567571
const datasetsRaw = flags.datasets as string;
@@ -818,7 +822,7 @@ async function runCreate<F extends FlagsDef>(
818822
if (!settings.dryRun && !profile.shouldSkipCapabilityCheck(modality)) {
819823
let capability: Awaited<ReturnType<typeof fetchModelCapability>> | undefined;
820824
try {
821-
capability = await fetchModelCapability(settings, model);
825+
capability = await fetchModelCapability(settings, model, diagnostics);
822826
} catch (error) {
823827
ctx.output.warning({
824828
code: "TRAINING_CAPABILITY_LOOKUP_FAILED",

‎packages/commands/src/commands/finetune/fee.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
effectiveConsoleGatewayConfig,
1414
unwrapResponse,
1515
MODEL_LIST_API,
16+
type HttpDeps,
1617
type Settings,
1718
type ModelPriceInfo,
1819
} from "bailian-cli-core";
@@ -31,6 +32,7 @@ export interface ActualFee {
3132
async function fetchTrainingPrice(
3233
settings: Settings,
3334
model: string,
35+
diagnostics: Pick<HttpDeps, "debug">,
3436
): Promise<ModelPriceInfo | null> {
3537
const eff = effectiveConsoleGatewayConfig(settings);
3638
const result = await callConsoleGateway(
@@ -55,6 +57,7 @@ async function fetchTrainingPrice(
5557
},
5658
},
5759
},
60+
diagnostics,
5861
);
5962
const responseData = unwrapResponse(result as Record<string, unknown>);
6063
const list = (responseData.list as Record<string, unknown>[]) ?? [];
@@ -90,9 +93,10 @@ export async function computeActualFee(
9093
settings: Settings,
9194
model: string,
9295
usageTokens: number,
96+
diagnostics: Pick<HttpDeps, "debug"> = {},
9397
): Promise<ActualFee | null> {
9498
try {
95-
const ftEntry = await fetchTrainingPrice(settings, model);
99+
const ftEntry = await fetchTrainingPrice(settings, model, diagnostics);
96100
const unitPrice = Number(ftEntry?.price);
97101
if (!Number.isFinite(unitPrice) || unitPrice <= 0) return null;
98102
const priceUnit = ftEntry?.priceUnit ?? "每百万tokens";

‎packages/commands/src/commands/finetune/get.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { defineCommand, getFineTune, type FlagsDef } from "bailian-cli-core";
1+
import { defineCommand, getFineTune, type HttpDeps, type FlagsDef } from "bailian-cli-core";
22

33
import { computeActualFee } from "./fee.ts";
44

@@ -23,6 +23,9 @@ export default defineCommand({
2323
exampleArgs: ["--job-id ft-xxx", "--job-id ft-xxx --output json"],
2424
async run(ctx) {
2525
const { settings, flags } = ctx;
26+
const diagnostics: Pick<HttpDeps, "debug"> = {
27+
debug: (diagnostic) => ctx.output.debug(diagnostic),
28+
};
2629
const jobId = flags.jobId;
2730

2831
if (settings.dryRun) {
@@ -77,7 +80,7 @@ export default defineCommand({
7780
// Actual fee: only when the platform reports a concrete token count
7881
// (SUCCEEDED / CANCELED). Best-effort — silently omitted on lookup failure.
7982
if (usageTokens !== undefined && usageTokens > 0 && job.model) {
80-
const fee = await computeActualFee(settings, job.model, usageTokens);
83+
const fee = await computeActualFee(settings, job.model, usageTokens, diagnostics);
8184
if (fee) {
8285
item.training_cost = fee.cost;
8386
item.cost_basis = `${fee.unitPrice} 元/${fee.priceUnit}`;

‎packages/commands/src/commands/finetune/watch.ts‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
getFineTune,
44
BailianError,
55
ExitCode,
6+
type HttpDeps,
67
type FlagsDef,
78
} from "bailian-cli-core";
89

@@ -134,6 +135,9 @@ export default defineCommand({
134135
],
135136
async run(ctx) {
136137
const { settings, flags } = ctx;
138+
const diagnostics: Pick<HttpDeps, "debug"> = {
139+
debug: (diagnostic) => ctx.output.debug(diagnostic),
140+
};
137141
const jobId = flags.jobId;
138142
const follow = flags.follow;
139143
const intervalSec = Math.max(MIN_INTERVAL_SEC, flags.interval ?? DEFAULT_INTERVAL_SEC);
@@ -175,7 +179,12 @@ export default defineCommand({
175179
const usageTokens = typeof job?.usage === "number" ? job.usage : undefined;
176180
if (terminal && usageTokens && usageTokens > 0 && job?.model) {
177181
output.usage_tokens = usageTokens;
178-
const fee = await computeActualFee(settings, job.model as string, usageTokens);
182+
const fee = await computeActualFee(
183+
settings,
184+
job.model as string,
185+
usageTokens,
186+
diagnostics,
187+
);
179188
if (fee) {
180189
output.training_cost = fee.cost;
181190
output.cost_basis = `${fee.unitPrice} 元/${fee.priceUnit}`;
@@ -226,7 +235,12 @@ export default defineCommand({
226235
const usageTokens = typeof job?.usage === "number" ? job.usage : undefined;
227236
const enriched: Record<string, unknown> = { ...response };
228237
if (usageTokens && usageTokens > 0 && job?.model) {
229-
const fee = await computeActualFee(settings, job.model as string, usageTokens);
238+
const fee = await computeActualFee(
239+
settings,
240+
job.model as string,
241+
usageTokens,
242+
diagnostics,
243+
);
230244
if (fee) {
231245
enriched.training_cost = fee.cost;
232246
enriched.usage_tokens = usageTokens;

‎packages/commands/tests/e2e/advisor-recommend.e2e.test.ts‎

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1-
import { describe, expect, test } from "vite-plus/test";
2-
import { isDashScopeE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
1+
import { afterEach, beforeEach, describe, expect, test, vi } from "vite-plus/test";
2+
import * as core from "bailian-cli-core";
3+
import {
4+
isDashScopeE2EReady,
5+
parseStdoutJson,
6+
runCommandHelp,
7+
runCommandE2e,
8+
runCommandInProcess,
9+
} from "./helpers.ts";
10+
import { E2E_HARNESS_IDENTITY } from "./harness/identity.ts";
311
import { ADVISOR_ROUTES } from "./topic-routes.ts";
412

513
describe("e2e: advisor recommend", () => {
@@ -14,6 +22,112 @@ describe("e2e: advisor recommend", () => {
1422
});
1523
});
1624

25+
describe("e2e: advisor recommend 传输(离线)", () => {
26+
beforeEach(() => {
27+
// 隔离目录、同步与原生网络,回归不读取真实目录或调用付费模型。
28+
vi.spyOn(core, "maybeSyncWikiData").mockResolvedValue(false);
29+
vi.spyOn(core, "loadCatalog").mockResolvedValue([]);
30+
vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("禁止原生网络请求")));
31+
});
32+
afterEach(() => {
33+
vi.restoreAllMocks();
34+
vi.unstubAllGlobals();
35+
});
36+
37+
test.each([true, false])(
38+
"真实命令注入统一传输且默认关闭 thinking,verbose=%s",
39+
async (verbose) => {
40+
const endpoint = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions";
41+
const requests: RequestInit[] = [];
42+
vi.spyOn(core.httpTransport, "fetch").mockImplementation(async (input, init) => {
43+
expect(input).toBe(endpoint);
44+
requests.push(init ?? {});
45+
const body = JSON.parse(init?.body as string);
46+
const content = JSON.stringify(
47+
requests.length === 1
48+
? {
49+
taskSummary: "文本生成",
50+
inputModality: ["Text"],
51+
outputModality: ["Text"],
52+
requiredCapabilities: ["TG"],
53+
matchedScenes: [],
54+
sceneConfidence: 1,
55+
}
56+
: { scores: [] },
57+
);
58+
const headers = { "x-request-id": "advisor-success-request" };
59+
return body.stream
60+
? new Response(
61+
`data: ${JSON.stringify({ choices: [{ delta: { content } }] })}\n\ndata: [DONE]\n\n`,
62+
{ headers },
63+
)
64+
: Response.json({ choices: [{ message: { content } }] }, { headers });
65+
});
66+
const { stdout, stderr, exitCode } = await runCommandInProcess(
67+
ADVISOR_ROUTES,
68+
[
69+
"advisor",
70+
"recommend",
71+
"--message",
72+
"文本生成",
73+
"--no-agent",
74+
...(verbose ? ["--verbose"] : ["--no-thinking"]),
75+
],
76+
{
77+
DASHSCOPE_API_KEY: "test-advisor-secret-key",
78+
DASHSCOPE_BASE_URL: "https://dashscope.aliyuncs.com",
79+
DO_NOT_TRACK: "1",
80+
NODE_OPTIONS: "",
81+
HTTP_PROXY: "",
82+
HTTPS_PROXY: "",
83+
http_proxy: "",
84+
https_proxy: "",
85+
},
86+
);
87+
expect(exitCode, stderr).toBe(0);
88+
expect(requests).toHaveLength(2);
89+
for (const request of requests) {
90+
const headers = new Headers(request.headers);
91+
expect(headers.get("authorization")).toBe("Bearer test-advisor-secret-key");
92+
expect(headers.get("user-agent")).toBe(
93+
`${E2E_HARNESS_IDENTITY.clientName}/${E2E_HARNESS_IDENTITY.version}`,
94+
);
95+
expect(headers.get("x-dashscope-source-config")).toBe(
96+
core.sourceConfig(E2E_HARNESS_IDENTITY),
97+
);
98+
expect(headers.get("x-dashscope-openapisource")).toBe("BailianCLI");
99+
expect(headers.get("x-dashscope-service")).toBe("bailian-cli");
100+
expect(JSON.parse(request.body as string).model).toBe(core.DEFAULT_LLM_MODEL);
101+
}
102+
expect(JSON.parse(requests[0].body as string).enable_thinking).toBeUndefined();
103+
expect(JSON.parse(requests[0].body as string).stream).toBeUndefined();
104+
expect(JSON.parse(requests[1].body as string).stream).toBeUndefined();
105+
expect(parseStdoutJson<{ intent: { taskSummary: string } }>(stdout).intent.taskSummary).toBe(
106+
"文本生成",
107+
);
108+
expect(stdout).not.toContain("request_id:");
109+
expect(stderr).not.toContain("test-advisor-secret-key");
110+
if (verbose) {
111+
const debugLines = stderr
112+
.trim()
113+
.split("\n")
114+
.filter((line) => line.startsWith('{"debug":'));
115+
expect(debugLines.length).toBeGreaterThanOrEqual(4);
116+
const requestDebug = JSON.parse(debugLines[0]!).debug;
117+
const responseDebug = JSON.parse(debugLines[1]!).debug;
118+
expect(requestDebug.details).toMatchObject({ method: "POST", url: endpoint });
119+
expect(responseDebug.details).toMatchObject({
120+
status: 200,
121+
request_id: "advisor-success-request",
122+
});
123+
} else {
124+
expect(stderr).not.toContain('{"debug":');
125+
expect(stderr).not.toContain("request_id:");
126+
}
127+
},
128+
);
129+
});
130+
17131
describe.skipIf(!isDashScopeE2EReady())("e2e: advisor recommend (DashScope)", () => {
18132
test("advisor recommend without --message errors as usage error (2)", async () => {
19133
const { stdout, stderr, exitCode } = await runCommandE2e(ADVISOR_ROUTES, [

0 commit comments

Comments
 (0)