mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 074fb58329 | |||
| 98acbd7341 | |||
| 125be085e5 | |||
| 186c500ca4 | |||
| 9ba4a9d1a3 | |||
| 50ed680ade |
@@ -88,6 +88,8 @@ import {
|
||||
tokenPlanCreateKey,
|
||||
tokenPlanAssignSeats,
|
||||
tokenPlanAddMember,
|
||||
tokenPlanPersonalUsage,
|
||||
tokenPlanPersonalKey,
|
||||
workspaceInit,
|
||||
pluginInstall,
|
||||
pluginLink,
|
||||
@@ -212,6 +214,8 @@ export const commands: Record<string, AnyCommand> = {
|
||||
"token-plan create-key": tokenPlanCreateKey,
|
||||
"token-plan assign-seats": tokenPlanAssignSeats,
|
||||
"token-plan add-member": tokenPlanAddMember,
|
||||
"token-plan personal-usage": tokenPlanPersonalUsage,
|
||||
"token-plan personal-key": tokenPlanPersonalKey,
|
||||
"workspace init": workspaceInit,
|
||||
"plugin install": pluginInstall,
|
||||
"plugin link": pluginLink,
|
||||
|
||||
@@ -50,6 +50,7 @@ export interface CredentialHost {
|
||||
*/
|
||||
export const CREDENTIALS_NOTE = [
|
||||
"Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).",
|
||||
"The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.",
|
||||
"Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.",
|
||||
"Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.",
|
||||
];
|
||||
@@ -85,13 +86,19 @@ export function prepareProviderEnv(): void {
|
||||
* the block references them and the interpolated value is empty (a literal in
|
||||
* agents.yaml is respected).
|
||||
*
|
||||
* `base_url` carries {@link AGENTSTUDIO_API_PATH} because the SDK appends resource
|
||||
* paths onto it verbatim; a value already ending in the suffix is left as-is.
|
||||
* It is filled even without a credential — `client.baseUrl` is readable
|
||||
* credential-less (defaults to the CLI's model-domain base URL) — so offline
|
||||
* commands (which skip the credential assert) still satisfy the SDK's
|
||||
* "workspace_id or base_url" schema. With no credential the `api_key` is left
|
||||
* untouched: online commands reject it via {@link assertProviderCredentials}.
|
||||
* `base_url` is composed from the workspace when one is known — block
|
||||
* `workspace_id` (agents.yaml literal or interpolated `${BAILIAN_WORKSPACE_ID}`)
|
||||
* first, then bl's configured `workspace_id` — because agentstudio is served
|
||||
* only on the workspace-scoped host; the bare model-domain origin 404s it
|
||||
* (managed-agents API overview: `https://{workspace_id}.cn-beijing.maas.
|
||||
* aliyuncs.com/api/v1/agentstudio`, region cn-beijing only). Only with no
|
||||
* workspace at all does the model-domain origin get {@link AGENTSTUDIO_API_PATH}
|
||||
* suffixed. A value already ending in the suffix is left as-is. base_url is
|
||||
* filled even without a credential — `client.baseUrl` is readable
|
||||
* credential-less — so offline commands (which skip the credential assert)
|
||||
* still satisfy the SDK's "workspace_id or base_url" schema. With no
|
||||
* credential the `api_key` is left untouched: online commands reject it via
|
||||
* {@link assertProviderCredentials}.
|
||||
*/
|
||||
export function injectProviderCredentials(
|
||||
providers: Record<string, unknown>,
|
||||
@@ -103,16 +110,27 @@ export function injectProviderCredentials(
|
||||
|
||||
const cred = host.client.exportApiCredential();
|
||||
if (cred) block.api_key = cred.token;
|
||||
if ("base_url" in block && !block.base_url) {
|
||||
// Defensive normalization: the auth chain already normalizes base_url to
|
||||
// an origin, but never let a trailing slash produce "//api/v1/agentstudio".
|
||||
const origin = host.client.baseUrl.replace(/\/+$/, "");
|
||||
block.base_url = origin.endsWith(AGENTSTUDIO_API_PATH)
|
||||
? origin
|
||||
: `${origin}${AGENTSTUDIO_API_PATH}`;
|
||||
if ("workspace_id" in block && !block.workspace_id) {
|
||||
// agents.yaml interpolation already replaced `${BAILIAN_WORKSPACE_ID}` in
|
||||
// file-based flows; the inline runtime passes an object config that never
|
||||
// interpolates, so read the env var here too (prepareProviderEnv
|
||||
// placeholders it to "" when unset). bl's configured workspace_id is the
|
||||
// last resort.
|
||||
block.workspace_id =
|
||||
process.env.BAILIAN_WORKSPACE_ID?.trim() || host.settings.workspaceId || "";
|
||||
}
|
||||
if ("workspace_id" in block && !block.workspace_id && host.settings.workspaceId) {
|
||||
block.workspace_id = host.settings.workspaceId;
|
||||
if ("base_url" in block && !block.base_url) {
|
||||
const workspaceId = typeof block.workspace_id === "string" ? block.workspace_id.trim() : "";
|
||||
if (workspaceId) {
|
||||
block.base_url = `https://${workspaceId}.cn-beijing.maas.aliyuncs.com${AGENTSTUDIO_API_PATH}`;
|
||||
} else {
|
||||
// Defensive normalization: the auth chain already normalizes base_url to
|
||||
// an origin, but never let a trailing slash produce "//api/v1/agentstudio".
|
||||
const origin = host.client.baseUrl.replace(/\/+$/, "");
|
||||
block.base_url = origin.endsWith(AGENTSTUDIO_API_PATH)
|
||||
? origin
|
||||
: `${origin}${AGENTSTUDIO_API_PATH}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,15 +56,17 @@ export function inlineStatePath(agentName: string): string {
|
||||
|
||||
/**
|
||||
* The minimal in-memory project config that materializes into one cloud agent.
|
||||
* `providers.bailian` carries empty `api_key`/`base_url` placeholders so
|
||||
* {@link injectProviderCredentials} fills them from bl's auth chain (it only
|
||||
* writes fields the block already declares).
|
||||
* `providers.bailian` carries empty `api_key`/`base_url`/`workspace_id`
|
||||
* placeholders so {@link injectProviderCredentials} fills them from bl's auth
|
||||
* chain and workspace sources (it only writes fields the block already
|
||||
* declares). `workspace_id` lets injection compose the workspace-scoped
|
||||
* agentstudio host instead of the model-domain origin.
|
||||
*/
|
||||
export function buildInlineConfig(opts: InlineAgentOptions): Record<string, unknown> {
|
||||
return {
|
||||
version: "1",
|
||||
providers: {
|
||||
bailian: { api_key: "", base_url: "" },
|
||||
bailian: { api_key: "", base_url: "", workspace_id: "" },
|
||||
},
|
||||
defaults: { provider: "bailian" },
|
||||
environments: {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
|
||||
const GET_KEY_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/api-keys/getKeyByUid";
|
||||
|
||||
export default defineCommand({
|
||||
description: "Get the personal-edition TokenPlan API key (masked) for the current account",
|
||||
auth: "console",
|
||||
usageArgs: "[flags]",
|
||||
flags: {},
|
||||
exampleArgs: [""],
|
||||
async run(ctx) {
|
||||
const { settings } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
const result = await ctx.client.console(GET_KEY_API, {});
|
||||
emitResult(result, format);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
|
||||
const USAGE_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage";
|
||||
const SUBSCRIPTION_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/subscription";
|
||||
const ADDON_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/addon/summary";
|
||||
|
||||
const COMMODITY_CN = "sfm_tokenplansolo_public_cn";
|
||||
const COMMODITY_INTL = "sfm_tokenplansolo_public_intl";
|
||||
const ADDON_CN = "sfm_tokenplansoloaddon_public_cn";
|
||||
const ADDON_INTL = "sfm_tokenplansoloaddon_public_intl";
|
||||
|
||||
function nested(obj: Record<string, unknown>, key: string): Record<string, unknown> | undefined {
|
||||
const val = obj[key];
|
||||
return val && typeof val === "object" && !Array.isArray(val)
|
||||
? (val as Record<string, unknown>)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/** Unwrap the console gateway `data.DataV2.data.data` envelope to the business payload. */
|
||||
function extract(result: Record<string, unknown>): Record<string, unknown> {
|
||||
const data = nested(result, "data");
|
||||
if (!data) return result;
|
||||
const dataV2 = nested(data, "DataV2");
|
||||
if (dataV2) {
|
||||
const inner = nested(dataV2, "data");
|
||||
const innerData = inner ? nested(inner, "data") : undefined;
|
||||
return innerData ?? inner ?? dataV2;
|
||||
}
|
||||
return nested(data, "data") ?? data;
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
description:
|
||||
"Query personal-edition TokenPlan usage (5h/1w percentage, subscription, addon credits)",
|
||||
auth: "console",
|
||||
usageArgs: "[flags]",
|
||||
flags: {},
|
||||
exampleArgs: [""],
|
||||
async run(ctx) {
|
||||
const { settings } = ctx;
|
||||
const format = detectOutputFormat(settings.output);
|
||||
const intl = settings.consoleSite === "international";
|
||||
|
||||
const [usage, subscription, addon] = await Promise.all([
|
||||
ctx.client.console(USAGE_API, {}),
|
||||
ctx.client.console(SUBSCRIPTION_API, {
|
||||
queryInstanceInfoRequest: { commodityCode: intl ? COMMODITY_INTL : COMMODITY_CN },
|
||||
}),
|
||||
ctx.client.console(ADDON_API, { commodityCode: intl ? ADDON_INTL : ADDON_CN }),
|
||||
]);
|
||||
|
||||
emitResult(
|
||||
{
|
||||
usage: extract(usage as Record<string, unknown>),
|
||||
subscription: extract(subscription as Record<string, unknown>),
|
||||
addonSummary: extract(addon as Record<string, unknown>),
|
||||
},
|
||||
format,
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -95,6 +95,8 @@ export { default as tokenPlanListSeats } from "./commands/token-plan/list-seats.
|
||||
export { default as tokenPlanCreateKey } from "./commands/token-plan/create-key.ts";
|
||||
export { default as tokenPlanAssignSeats } from "./commands/token-plan/assign-seats.ts";
|
||||
export { default as tokenPlanAddMember } from "./commands/token-plan/add-member.ts";
|
||||
export { default as tokenPlanPersonalUsage } from "./commands/token-plan/personal-usage.ts";
|
||||
export { default as tokenPlanPersonalKey } from "./commands/token-plan/personal-key.ts";
|
||||
export { default as managedAgentInit } from "./commands/managed-agent/init.ts";
|
||||
export { default as managedAgentValidate } from "./commands/managed-agent/validate.ts";
|
||||
export { default as managedAgentPlan } from "./commands/managed-agent/plan.ts";
|
||||
|
||||
@@ -124,7 +124,8 @@ test("inject:已带后缀且尾斜杠的 base_url 去斜杠后原样保留", ()
|
||||
expect(providers.bailian.base_url).toBe("https://x.maas.aliyuncs.com/api/v1/agentstudio");
|
||||
});
|
||||
|
||||
test("inject:workspace_id 引用且为空时用 settings 填充;有字面量则保留", () => {
|
||||
test("inject:workspace_id 引用且为空时按 env > settings 填充;有字面量则保留", () => {
|
||||
delete process.env.BAILIAN_WORKSPACE_ID;
|
||||
const empty = { bailian: { api_key: "", workspace_id: "" } };
|
||||
injectProviderCredentials(
|
||||
empty,
|
||||
@@ -132,6 +133,16 @@ test("inject:workspace_id 引用且为空时用 settings 填充;有字面量则
|
||||
);
|
||||
expect(empty.bailian.workspace_id).toBe("ws-settings");
|
||||
|
||||
// 内联运行时(对象配置)不做 ${} 插值,env 变量在此补读。
|
||||
process.env.BAILIAN_WORKSPACE_ID = "ws-env";
|
||||
const fromEnv = { bailian: { api_key: "", workspace_id: "" } };
|
||||
injectProviderCredentials(
|
||||
fromEnv,
|
||||
makeHost({ apiCred: bailianCred(), workspaceId: "ws-settings" }),
|
||||
);
|
||||
expect(fromEnv.bailian.workspace_id).toBe("ws-env");
|
||||
delete process.env.BAILIAN_WORKSPACE_ID;
|
||||
|
||||
const literal = { bailian: { api_key: "", workspace_id: "ws-yaml" } };
|
||||
injectProviderCredentials(
|
||||
literal,
|
||||
@@ -140,6 +151,37 @@ test("inject:workspace_id 引用且为空时用 settings 填充;有字面量则
|
||||
expect(literal.bailian.workspace_id).toBe("ws-yaml");
|
||||
});
|
||||
|
||||
test("inject:workspace 已知时 base_url 拼工作空间主机,而非模型域 origin", () => {
|
||||
// agents.yaml 字面量 workspace_id + 空 base_url。
|
||||
const literal = { bailian: { api_key: "", base_url: "", workspace_id: "ws-yaml" } };
|
||||
injectProviderCredentials(literal, makeHost({ apiCred: bailianCred() }));
|
||||
expect(literal.bailian.base_url).toBe(
|
||||
"https://ws-yaml.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio",
|
||||
);
|
||||
|
||||
// 内联块:workspace_id 由 settings 填充后同样走工作空间主机。
|
||||
const inline = { bailian: { api_key: "", base_url: "", workspace_id: "" } };
|
||||
injectProviderCredentials(
|
||||
inline,
|
||||
makeHost({ apiCred: bailianCred(), workspaceId: "ws-settings" }),
|
||||
);
|
||||
expect(inline.bailian.workspace_id).toBe("ws-settings");
|
||||
expect(inline.bailian.base_url).toBe(
|
||||
"https://ws-settings.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio",
|
||||
);
|
||||
|
||||
// 显式 base_url 字面量永远优先于拼装。
|
||||
const explicit = {
|
||||
bailian: {
|
||||
api_key: "",
|
||||
base_url: "https://custom.example.com/api/v1/agentstudio",
|
||||
workspace_id: "ws-yaml",
|
||||
},
|
||||
};
|
||||
injectProviderCredentials(explicit, makeHost({ apiCred: bailianCred() }));
|
||||
expect(explicit.bailian.base_url).toBe("https://custom.example.com/api/v1/agentstudio");
|
||||
});
|
||||
|
||||
test("inject:无凭证时 api_key 保持不变,base_url 仍用 client 默认域名补齐(离线/范围外 schema 可用)", () => {
|
||||
const providers = { bailian: { api_key: "", base_url: "" } };
|
||||
injectProviderCredentials(providers, makeHost({}));
|
||||
|
||||
@@ -162,6 +162,8 @@ export const TOKEN_PLAN_ROUTES: E2eRouteExports = {
|
||||
"token-plan create-key": "tokenPlanCreateKey",
|
||||
"token-plan assign-seats": "tokenPlanAssignSeats",
|
||||
"token-plan add-member": "tokenPlanAddMember",
|
||||
"token-plan personal-usage": "tokenPlanPersonalUsage",
|
||||
"token-plan personal-key": "tokenPlanPersonalKey",
|
||||
};
|
||||
|
||||
export const SKILL_ROUTES: E2eRouteExports = {
|
||||
|
||||
@@ -199,7 +199,15 @@ export function buildSources(flags: Partial<SourceFlags>): ResolutionSources {
|
||||
const raw = readRawConfigObject();
|
||||
const configExplicit = flags.config !== undefined;
|
||||
const activeConfigName = readStoredActiveConfigName(raw, !configExplicit);
|
||||
const configName = configExplicit ? normalizeConfigName(flags.config) : activeConfigName;
|
||||
// Config selection: --config flag > BAILIAN_CONFIG env > persisted active_config.
|
||||
// The env lets a host (e.g. dsh) pin a named profile for all child `bl`
|
||||
// calls without rewriting --config or the user's active_config.
|
||||
const envConfig = process.env.BAILIAN_CONFIG;
|
||||
const configName = configExplicit
|
||||
? normalizeConfigName(flags.config)
|
||||
: envConfig
|
||||
? normalizeConfigName(envConfig)
|
||||
: activeConfigName;
|
||||
return {
|
||||
flags,
|
||||
file: parseConfigFile(readRawConfigBlock(raw, configName)),
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# build artifacts (regenerated by `pnpm build`)
|
||||
client.bundle.js
|
||||
dist/
|
||||
*.tgz
|
||||
+147
-165
@@ -2,79 +2,46 @@
|
||||
|
||||
把阿里云百炼(Model Studio)的能力接入 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`)的 profile bundle。
|
||||
|
||||
一个包提供 5 个插件行,外加对 base bundle 的 `llm-pi-ai` 行做一次配置覆盖:
|
||||
本包提供两项能力:
|
||||
|
||||
| row id | 能力 | 默认 | 依赖 |
|
||||
| ---------------------------- | --------------------------------------------------------------- | ---- | --------------------- |
|
||||
| `llm-pi-ai`(覆盖 base 行) | 把百炼 TokenPlan 网关注册成 LLM provider(`bailian-tokenplan`) | 启用 | TokenPlan Key |
|
||||
| `bailian-tool-vision` | `bailian_vision_describe`:图片/视频理解 | 启用 | `bl` |
|
||||
| `bailian-tool-image` | `bailian_image_generate`:文生图 | 启用 | `bl` |
|
||||
| `bailian-tool-managed-agent` | `bailian_run_remote_task`:按需在云端创建 agent 并跑任务 | 启用 | `bl` + 按量付费 Key |
|
||||
| `bailian-web-search-rag` | 百炼知识库检索,注册为 `web_search` 的后端 | 停用 | 按量付费 Key + 知识库 |
|
||||
| `bailian-memory` | 跨会话长期记忆(tools + 自动检索/落库) | 停用 | 按量付费 Key |
|
||||
|
||||
`web-search-rag` 与 `memory` 默认停用是有意的:它们要么需要部署方特有的资源 ID,要么按次计费,不该在用户没配置时就生效。`tool-managed-agent` 默认启用——它加载时不建任何资源,只有模型真正调用时才在云端创建 agent。
|
||||
| 能力 | 说明 |
|
||||
| ------------------ | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Bailian 设置页** | 通用的百炼凭证配置(AK/SK 存入 `dsh` bl profile + DashScope API Key)+ TokenPlan 用量展示 + 记忆库配置 + 新会话欢迎页 |
|
||||
| **跨会话长期记忆** | 自动检索注入 + 自动落库,模型可主动 search/add/list。按量计费,默认停用 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 前置条件
|
||||
|
||||
- Node ≥ 22.19(`dsh` 的要求)
|
||||
- `bl`(vision / image / 远程任务 三个工具通过子进程调它)
|
||||
- `bl`(用量展示通过子进程调用 `bl console call`)
|
||||
|
||||
```sh
|
||||
npm install -g bailian-cli
|
||||
```
|
||||
|
||||
- 百炼 API Key。**注意有两类且不可混用**:
|
||||
- **阿里云 AK/SK**(AccessKey ID + AccessKey Secret)—— 用于控制台鉴权,查询用量信息。在 webui 设置页填入即可,无需环境变量。
|
||||
- **DashScope API Key**(`sk-` 前缀,按量付费)—— 用于记忆库等 DashScope API 调用。在设置页「凭证配置」填入,与 AK/SK 并列为通用凭证。
|
||||
|
||||
| 类型 | 前缀 | 能访问 | 不能访问 |
|
||||
| --------- | -------- | --------------------------------------- | -------------- |
|
||||
| TokenPlan | `sk-sp-` | TokenPlan 网关(LLM / vision / 文生图) | 记忆库、知识库 |
|
||||
| 按量付费 | `sk-ws-` | 记忆库、知识库、DashScope 全量接口 | TokenPlan 网关 |
|
||||
|
||||
两者互相返回 `401 InvalidApiKey`,所以本包用**两个不同的环境变量**,不会互相踩:
|
||||
|
||||
```sh
|
||||
export BAILIAN_TOKENPLAN_API_KEY=sk-sp-xxx # 只给 bailian-tokenplan provider
|
||||
export DASHSCOPE_API_KEY=sk-ws-xxx # 给 bl、memory、RAG
|
||||
```
|
||||
|
||||
只有一类 Key 也能用,只是能力范围相应缩小。若只有 TokenPlan Key:
|
||||
|
||||
```sh
|
||||
export BAILIAN_TOKENPLAN_API_KEY=sk-sp-xxx
|
||||
export DASHSCOPE_API_KEY=sk-sp-xxx
|
||||
export DASHSCOPE_BASE_URL=https://token-plan.cn-beijing.maas.aliyuncs.com
|
||||
```
|
||||
|
||||
这样 LLM / vision / 文生图可用,memory 与 RAG 不可用(保持停用即可)。
|
||||
获取方式:[阿里云控制台 → AccessKey 管理](https://ram.console.aliyun.com/manage/ak)
|
||||
|
||||
---
|
||||
|
||||
## 2. 安装到 `web` profile
|
||||
|
||||
`npx @deepseek-ai/dsh web` 是 `dsh --profile web` 的别名,所以要装进**名为 `web` 的 profile**,配置目录是 `~/.dsh/profiles/web/`(`$DSH_HOME` 可覆盖)。
|
||||
|
||||
本包尚未发布到 npm,先在本仓库打包:
|
||||
`npx @deepseek-ai/dsh web` 是 `dsh --profile web` 的别名,配置目录是 `~/.dsh/profiles/web/`。
|
||||
|
||||
```sh
|
||||
pnpm -F bailian-cli-dsh build
|
||||
cd packages/dsh && pnpm pack # 产出 bailian-cli-dsh-<version>.tgz
|
||||
pnpm -F bailian-cli-dsh build # vp pack(host)+ esbuild(client.bundle.js)
|
||||
cd packages/dsh && pnpm pack
|
||||
|
||||
npx @deepseek-ai/dsh plugin --profile web add /absolute/path/to/bailian-cli-dsh-<version>.tgz
|
||||
```
|
||||
|
||||
装入 profile(`dsh plugin` 是 pnpm 的转发器,接受本地路径 / tarball / npm 包名 / git):
|
||||
确认 bailian 行都在:
|
||||
|
||||
```sh
|
||||
npx @deepseek-ai/dsh plugin --profile web add /absolute/path/to/bailian-cli-dsh-1.14.2.tgz
|
||||
```
|
||||
|
||||
因为 `package.json` 声明了 `dsh.bundle`,安装后会自动加入该 profile 的 bundle 层,无需手动改 `cordis.patch.yml`。
|
||||
|
||||
确认 5 个插入行都在,且 TokenPlan provider 已配到 `llm-pi-ai` 上:
|
||||
|
||||
```sh
|
||||
npx @deepseek-ai/dsh --profile web --dump-config | grep -E 'bailian|tokenplan'
|
||||
npx @deepseek-ai/dsh --profile web --dump-config | grep -E 'bailian'
|
||||
```
|
||||
|
||||
启动:
|
||||
@@ -85,159 +52,134 @@ npx @deepseek-ai/dsh web
|
||||
|
||||
Web UI 在 http://127.0.0.1:3080。
|
||||
|
||||
> 发布到 npm 后直接 `npx @deepseek-ai/dsh plugin --profile web add bailian-cli-dsh`,跳过打包步骤。
|
||||
---
|
||||
|
||||
## 3. Bailian 设置页 + 欢迎页
|
||||
|
||||
安装并重启后:
|
||||
|
||||
- **Settings → Bailian**:通用设置页(凭证配置 / TokenPlan 用量 / 记忆库)。
|
||||
- **新会话欢迎页**:每个新会话(blank)在输入框上方显示「百炼 Agent」欢迎页(Tab + 功能卡片),发出第一条消息后自动隐藏。
|
||||
|
||||
### 凭证配置(通用)
|
||||
|
||||
1. 在「凭证配置」区填入 **AccessKey ID** 和 **AccessKey Secret**
|
||||
2. 点击 **「保存凭证」**
|
||||
|
||||
Host 会执行 `bl auth login --open-api --config dsh`,将 AK/SK 和新生成的 access_token 存入 bl 的 `dsh` 专属 profile。**所有后续百炼插件共用此凭证**,无需重复配置。
|
||||
|
||||
### TokenPlan 用量
|
||||
|
||||
1. 选择区域和站点
|
||||
2. 点击 **「查询用量」**
|
||||
|
||||
Host 执行 `bl console call --config dsh` 调用 3 个个人版控制台接口,返回:
|
||||
|
||||
- **用量百分比** —— 5 小时窗口 / 1 周窗口的用量百分比和重置时间
|
||||
- **套餐信息** —— 套餐类型(基础版/标准版/高级版)、状态、剩余天数、到期时间、自动续费
|
||||
- **额外用量包** —— Credits 总量、剩余量、生效中数量
|
||||
|
||||
### 凭证解析优先级
|
||||
|
||||
凭证保存到 bl 的 `dsh` profile 后,所有百炼插件通过 `--config dsh` 读取。行内 config 的 `accessKeyId`/`accessKeySecret` 作为兜底(未通过 UI 保存时自动使用)。
|
||||
|
||||
### 行内配置(可选)
|
||||
|
||||
如果不想在 UI 里每次输入,可以在 profile 的 `cordis.patch.yml` 里固化凭证:
|
||||
|
||||
```yaml
|
||||
- id: bailian-tokenplan-usage
|
||||
config:
|
||||
# accessKeyId / accessKeySecret: 兜底凭证(未通过 UI 保存时使用)
|
||||
# consoleRegion: cn-beijing
|
||||
# consoleSite: domestic
|
||||
# profile: dsh # 默认用 dsh 专属 profile
|
||||
```
|
||||
|
||||
配置后 UI 表单会留空,但点击「查询用量」会使用行内凭证。
|
||||
|
||||
---
|
||||
|
||||
## 3. 开箱能用的部分
|
||||
## 4. 跨会话长期记忆
|
||||
|
||||
装完不做任何配置就生效:
|
||||
默认停用(按量计费)。在 `cordis.patch.yml` 中设 `disabled: false` 启用,然后在设置页配置 API Key 和参数。
|
||||
|
||||
**LLM provider** — 模型选择器里出现 `bailian-tokenplan`,可选模型(已逐个实测):
|
||||
### 功能
|
||||
|
||||
| 模型 | 读图 |
|
||||
| ------------------------ | ------------------ |
|
||||
| `qwen3.8-max` | 是 |
|
||||
| `qwen3.7-plus` | 是 |
|
||||
| `qwen3.6-flash` | 是 |
|
||||
| `glm-5.2` | 是 |
|
||||
| `qwen3.7-max` | 否(传图直接 400) |
|
||||
| `deepseek-v4-pro` | 否 |
|
||||
| `deepseek-v4-flash-0731` | 否 |
|
||||
- **自动检索注入**:新会话首轮,用用户消息搜索记忆,将结果注入上下文(`autoInject`,默认开启)
|
||||
- **自动落库**:每轮结束,将该轮新消息发送到记忆库 add API(`autoPersist`,默认开启)
|
||||
- **模型工具**:`bailian_memory_search`(检索)、`bailian_memory_add`(存储)、`bailian_memory_list`(浏览)
|
||||
|
||||
**三个工具** — `bailian_vision_describe`、`bailian_image_generate`、`bailian_run_remote_task`。
|
||||
### 触发机制
|
||||
|
||||
前两个走 TokenPlan(vision/image)。`bailian_run_remote_task` 见 [§3.1](#31-远程任务-bailian_run_remote_task)——它默认启用但用的是**按量付费 Key + dashscope 端点**,与 TokenPlan 那两个不同。
|
||||
| 时机 | 触发方式 |
|
||||
| ---------- | --------------------------------------------------------- |
|
||||
| 新会话首轮 | 自动检索记忆注入上下文(`agent/pre-step` 事件) |
|
||||
| 对话中 | 模型主动调用 `bailian_memory_search`/`bailian_memory_add` |
|
||||
| 轮次结束 | 自动落库新消息(`agent/turn-stopping` 事件) |
|
||||
|
||||
### 关于看图,有个坑值得知道
|
||||
### 凭证与配置
|
||||
|
||||
dsh 会在两处**提前**拦截图片:Web UI 粘图前会查当前模型的输入模态,`read_image` 也有同样的门禁。所以主模型选 DeepSeek 时,图片根本进不到对话里。
|
||||
- **API Key**:DashScope 按量付费 Key(`sk-`),在设置页「凭证配置」填入
|
||||
- **Base URL**:默认 `https://dashscope.aliyuncs.com/api/v2/apps/memory/`
|
||||
- **User ID**:记忆归属 ID,默认读系统用户名
|
||||
- **Plan Version**:`lite`(便宜,关闭 rerank)或 `pro`(开启 rerank,约 50 倍成本)。注意:实际计费由 `enable_rerank` 控制
|
||||
- **Top K**:检索返回数量(1-100,默认 10)
|
||||
- **Memory Library ID**:记忆库 ID,留空用默认
|
||||
|
||||
- 主模型选 `qwen3.8-max` 等标着"是"的 → 直接粘图,原生看图,不需要任何工具
|
||||
- 主模型选 DeepSeek → 让它调 `bailian_vision_describe`,工具返回**文字描述**,绕过模态门禁
|
||||
### 计费
|
||||
|
||||
DeepSeek 那两个模型在 TokenPlan 网关上传图**不报错但也看不见**(实测会回答 "None"),所以本包坚决没给它们声明 `input: [image]`——否则会从"明确拒绝"退化成"静默失明",更难排查。
|
||||
- Add:120 QPM
|
||||
- Search:300 QPM(Lite ¥0.00002/次,Pro ¥0.001/次)
|
||||
- 总计不超过 3000 QPM
|
||||
|
||||
`bailian_image_generate` 同理:模型能看图时返回内联图片,不能看图时降级为返回落盘路径,你可以接着用 vision 工具读它。文件不会被删除,正是为了这个衔接。
|
||||
|
||||
### 3.1 远程任务(`bailian_run_remote_task`)
|
||||
|
||||
把一个任务甩到百炼云端的托管 agent 上跑,不占本地会话。**无需预先写 `agents.yaml` 或 `apply`**:工具首次被调用时,`bl managed-agent run` 会在你的账号里幂等创建一个 agent + cloud environment,之后复用。
|
||||
|
||||
- 模型自己按用户意图填 `instructions`(远程 agent 的角色),`task` 是要它做的事。例如你说「在云端帮我审计这个依赖树,它该懂安全」→ 模型调 `bailian_run_remote_task(task="审计依赖树", instructions="你是安全专家")`。
|
||||
- **前提**:这条路走的是 managed-agent(agentstudio)服务,需要**按量付费 Key**(`sk-ws-`)+ dashscope 端点,且账号已开通 managed-agent。TokenPlan Key 不适用。若 `DASHSCOPE_API_KEY`/端点没配好,首次调用会返回 `Bailian API 404`。
|
||||
- 首次会创建云资源(可能计费、启动有延迟);同名 agent 后续复用。默认 agent 名 `dsh-remote-runner`,可在配置里改。
|
||||
|
||||
需要非默认的 agent 名 / 模型时:
|
||||
|
||||
```yaml
|
||||
- id: bailian-tool-managed-agent
|
||||
config:
|
||||
agent: my-runner
|
||||
model: qwen3.8-max
|
||||
timeoutMs: 600000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 开启可选插件
|
||||
|
||||
用户层配置写在 `~/.dsh/profiles/web/cordis.patch.yml`,按 row `id` 覆盖 bundle 的默认值。
|
||||
|
||||
> **一个必须记住的语义**:patch 是按 row **整体替换 `config`**,不是深合并。所以覆盖一行时要把该行完整的 config 重写一遍。
|
||||
|
||||
### 知识库检索(RAG)
|
||||
|
||||
注册 id 为 `bailian-kb` 的搜索后端,模型用它熟悉的 `web_search` 就能检索私域文档。
|
||||
|
||||
```yaml
|
||||
- id: bailian-web-search-rag
|
||||
disabled: false
|
||||
config:
|
||||
workspaceId: llm-xxxxxxxx # 百炼控制台工作空间 ID
|
||||
agentId: aid-xxxxxxxx # 知识库"检索服务"ID
|
||||
maxResults: 10
|
||||
# apiKey 省略则读 $DASHSCOPE_API_KEY
|
||||
```
|
||||
|
||||
一个实例对一个知识库(`WebSearchRequest` 只带 `query` / `maxResults`,agentId 只能来自配置)。要多个知识库就插多行不同 `id`。
|
||||
|
||||
**如果 profile 里还有别的搜索 provider**(base bundle 默认带 `web-search-deepseek`),必须显式指定用哪个,否则 dsh 报 `WEB_PROVIDER_AMBIGUOUS`:
|
||||
|
||||
```yaml
|
||||
- id: web
|
||||
config:
|
||||
searchProvider: bailian-kb
|
||||
```
|
||||
|
||||
### 长期记忆
|
||||
|
||||
dsh 自身没有跨会话记忆(`ctx.compaction` 只在单会话内压缩上下文)。开启后:两个工具 `bailian_memory_search` / `bailian_memory_add`,加上每个会话首轮自动检索注入、每轮结束自动落库。
|
||||
### 启用
|
||||
|
||||
```yaml
|
||||
- id: bailian-memory
|
||||
disabled: false
|
||||
config:
|
||||
userId: your-name # 省略则读 $BAILIAN_MEMORY_USER_ID,再退到系统用户名
|
||||
planVersion: lite
|
||||
baseUrl: "https://dashscope.aliyuncs.com/api/v2/apps/memory/"
|
||||
planVersion: "lite"
|
||||
topK: 10
|
||||
autoInject: true
|
||||
injectEveryTurn: false # 开启会变成每轮一次检索,成本相应上升
|
||||
autoPersist: true
|
||||
```
|
||||
|
||||
**费用**:记忆库自 2026-08-20 起商业化,add 与 search 按次计费,pro 档约为 lite 档的 50 倍。
|
||||
启用后在设置页「记忆库」section 配置 API Key 和参数。
|
||||
|
||||
实测发现一个与文档不符的地方:单独传 `plan_version: lite` 会被服务端忽略、仍按 pro 计费,真正生效的开关是 `enable_rerank: false`。本插件已按此处理——`planVersion: lite`(默认)会同时下发 `enable_rerank: false`,所以默认就是便宜的那档。
|
||||
|
||||
不想要自动行为、只保留手动工具:
|
||||
|
||||
```yaml
|
||||
- id: bailian-memory
|
||||
disabled: false
|
||||
config:
|
||||
userId: your-name
|
||||
autoInject: false
|
||||
autoPersist: false
|
||||
```
|
||||
|
||||
> 远程任务(`bailian_run_remote_task`)默认启用,配置见 [§3.1](#31-远程任务-bailian_run_remote_task)。
|
||||
|
||||
---
|
||||
> 记忆库调用 DashScope memory v2 API(非 `bl memory`),因为 v2 API 暴露了 `min_score`、`enable_rerank`、`plan_version`、`memory_library_id` 等参数 `bl memory` 不支持。
|
||||
|
||||
## 5. 验证
|
||||
|
||||
```sh
|
||||
# 配置是否被正确合成(改完 patch 后先看这个)
|
||||
npx @deepseek-ai/dsh --profile web --dump-config | grep -A5 bailian-memory
|
||||
# 配置合成
|
||||
npx @deepseek-ai/dsh --profile web --dump-config | grep bailian
|
||||
|
||||
# bl 是否就绪
|
||||
# bl 就绪
|
||||
bl auth status
|
||||
```
|
||||
|
||||
启动后逐项试:
|
||||
启动后验证:
|
||||
|
||||
- **LLM**:切到 `bailian-tokenplan / qwen3.8-max`,随便发一句
|
||||
- **原生看图**:同上模型,直接粘一张图提问
|
||||
- **间接看图**:切到 `deepseek-v4-pro`,让它用 `bailian_vision_describe` 读同一张图
|
||||
- **文生图**:让模型生成一张图
|
||||
- **RAG**:问一个只有知识库里才有答案的问题
|
||||
- **记忆**:会话 A 告诉它一个事实 → 关掉 → 新开会话 B 提问,看是否命中
|
||||
- **远程任务**:说「在云端帮我跑一个任务:<something>,它该擅长 <role>」→ 确认模型调用 `bailian_run_remote_task`(`instructions` 由模型按 role 填)→ 首次触发云端创建 → 返回远程会话结果(需按量付费 Key + 已开通 agentstudio)
|
||||
- **欢迎页**:新开一个会话,输入框上方出现「百炼 Agent」欢迎页
|
||||
- **凭证配置**:打开 Settings → Bailian → 填入 AK/SK → 保存凭证
|
||||
- **用量展示**:同页面选择区域 → 查询用量
|
||||
- **记忆库**:启用 `bailian-memory` 后,同页面配置 API Key
|
||||
|
||||
---
|
||||
|
||||
## 6. 常见问题
|
||||
|
||||
| 现象 | 原因 |
|
||||
| -------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| LLM 路由 `401 InvalidApiKey` | `BAILIAN_TOKENPLAN_API_KEY` 没设,或误填了 `sk-ws-` 的按量付费 Key |
|
||||
| memory / RAG `401 InvalidApiKey` | `DASHSCOPE_API_KEY` 误填了 `sk-sp-` 的 TokenPlan Key |
|
||||
| `WEB_PROVIDER_AMBIGUOUS` | 有多个搜索 provider,需在 `web` 行 pin `searchProvider` |
|
||||
| 粘图报 `MODEL_DOES_NOT_SUPPORT_IMAGES` | 当前模型不支持图片输入,换成上表标"是"的,或改用 vision 工具 |
|
||||
| 工具报找不到 `bl` | `bl` 不在 PATH:`npm install -g bailian-cli` |
|
||||
| 改了 patch 但没生效 | `config` 是整体替换,检查是否漏写了原有字段;再用 `--dump-config` 确认 |
|
||||
| `memoryLibraryId does not exist` | 记忆库 ID 属于另一个账号,与当前 Key 不匹配 |
|
||||
| 现象 | 原因 |
|
||||
| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 用量查询报 `bl auth login failed` | AK/SK 无效或无权限;确认 AK 有百炼控制台访问权限 |
|
||||
| 用量查询报 `NotLogined` 或 token 过期 | bl 的 access token 已过期;Host 会自动通过 AK/SK 刷新,确认 AK/SK 正确 |
|
||||
| 用量查询报 `bl console call failed` | 控制台接口调用失败;检查 region/site 是否匹配你的账号 |
|
||||
| 用量查询报 `Workspace.NotAuthorised` | bl 用了其他 profile 的旧 access_token;Host 默认用 `--config dsh` 专属 profile 隔离,首次 login 会生成新 token |
|
||||
| 工具报找不到 `bl` | `bl` 不在 PATH:`npm install -g bailian-cli` |
|
||||
| 设置页/欢迎页看不到 Bailian | 需**重启 `dsh web`**(bundle 在启动时加载);确认 `dump-config` 有 `bailian-client` 行,且 `client.bundle.js` 为 ModuleLoader 格式 |
|
||||
| 启动报 `invalid plugin ... apply` | 包根 `dist/index.mjs` 必须导出 `apply`(no-op 插件);重新 `pnpm build` 再装 |
|
||||
|
||||
---
|
||||
|
||||
@@ -247,4 +189,44 @@ bl auth status
|
||||
npx @deepseek-ai/dsh plugin --profile web remove bailian-cli-dsh
|
||||
```
|
||||
|
||||
移除后 bundle 层会自动从 `dsh.profile.bundles` 摘掉;`~/.dsh/profiles/web/cordis.patch.yml` 里你手写的覆盖行需要自己清理。
|
||||
---
|
||||
|
||||
## 架构说明
|
||||
|
||||
### Host 半
|
||||
|
||||
- `src/tokenplan-usage/index.ts` —— 凭证 + TokenPlan 用量。`inject: ['subprocess']`,所有 bl 命令带 `--config dsh` 隔离凭证。两个 webServer 路由:
|
||||
- `POST /bailian/credentials` — 保存 AK/SK(`bl auth login --open-api --config dsh`,生成新 token)
|
||||
- `POST /bailian/tokenplan/usage` — 查询用量(`bl console call --config dsh`,3 个个人版接口)
|
||||
- `src/memory/index.ts` —— 记忆库(默认停用)。直接调 DashScope memory v2 API,注册 tools + auto-inject/persist。路由 `/bailian/memory/config`、`/bailian/memory/status`。
|
||||
- `src/index.ts` —— 包根 no-op 插件,供 `bailian-client` 行加载(该行只为了让 client-modules 服务浏览器 bundle)。
|
||||
|
||||
> 路由用 `/bailian/*` 而非 `/api/*`:`/api` 前缀被 dsh 的 RPC 网关(apiProxy)占用,自定义路由会被遮蔽。
|
||||
|
||||
调用链路:**AK/SK → `bl auth login --open-api --config dsh`(存入 dsh profile)→ `bl console call --config dsh`(读 dsh profile token → 控制台网关)→ 个人版 TokenPlan 接口**
|
||||
|
||||
### Client 半(`src/client.ts`)
|
||||
|
||||
- 唯一的浏览器源码,构建为 DSH ModuleLoader 格式(见下)。
|
||||
- 注册 `settings.section`(id: `bailian`,label: `Bailian`),渲染通用百炼设置页(凭证配置 / TokenPlan 用量 / 记忆库)。
|
||||
- 注册 `conversation.input.dock`(id: `bailian-welcome`):当 `session.blank === true`(新会话)渲染「百炼 Agent」欢迎页(Tab + 功能卡片),开始对话后自动隐藏。
|
||||
- 通过 `fetch('/bailian/*')` 调 Host 路由。
|
||||
|
||||
### Client 构建(ModuleLoader 格式)
|
||||
|
||||
DSH 浏览器只加载 `window.__ModuleLoader__.load({ id, factory })` 格式的 bundle(`require('react')` 由浏览器 ModuleLoader 提供)。vite-plus 产出裸 ES module,格式不对,所以 client 单独用 esbuild 构建:
|
||||
|
||||
- `scripts/build-client.mjs` —— 把 `src/client.ts` 构建为 CJS + browser + `react` external,包上 ModuleLoader banner/footer,输出 `client.bundle.js`。
|
||||
- `package.json` 的 `build` = `vp pack && node scripts/build-client.mjs`。
|
||||
- `package.json` 的 `exports["./client"]` 与 `dsh.client: { platform: "web" }` 指向 `client.bundle.js`,被 client-modules 扫描并服务。
|
||||
- `cordis.patch.yml` 的 `bailian-client` 行 `name` 必须是**包根**(`bailian-cli-dsh`,无子路径),client-modules 才能 `require.resolve("<name>/package.json")` 识别 `dsh.client`。
|
||||
|
||||
改 client UI 只需编辑 `src/client.ts`,`pnpm build` 自动重新生成 `client.bundle.js`。
|
||||
|
||||
### 共享模块(`src/shared/`)
|
||||
|
||||
- `bl.ts` —— `bl` 子进程调用封装(env 转发、stdout/stderr 收集、JSON 解析)
|
||||
- `credentials.ts` —— TokenPlan / 按量付费 Key 分类工具
|
||||
- `http.ts` —— DashScope HTTP 客户端
|
||||
|
||||
这些模块来自早期版本(vision / image / managed-agent / RAG / memory 工具),已移除工具实现但保留共享逻辑作为参考。
|
||||
|
||||
@@ -1,97 +1,53 @@
|
||||
# bailian-cli-dsh — Aliyun Model Studio (Bailian) as a dsh profile bundle.
|
||||
#
|
||||
# Applied over whatever the earlier layers composed: one config override of the
|
||||
# base bundle's dormant pi-ai adapter, then one insert of the Bailian tool rows.
|
||||
# Inserts Bailian plugin rows: TokenPlan usage display + cross-session memory.
|
||||
# Every inserted id is `bailian-`-prefixed so a user profile can address,
|
||||
# reconfigure, or disable any single capability without touching the others.
|
||||
# Remember that a later patch REPLACES a row's whole `config` rather than
|
||||
# merging into it, so restate the complete config when overriding.
|
||||
|
||||
# TokenPlan configures the base bundle's existing `llm-pi-ai` row instead of
|
||||
# inserting a second @deepseek-ai/dsh-llm-pi-ai instance. That plugin declares
|
||||
# pi-ai's entire built-in provider catalog to
|
||||
# `ctx.llm.registerConfigurableProviders` on every apply, and that registry
|
||||
# refuses an already-declared provider, so a second instance always fails the
|
||||
# whole tree with DUPLICATE_DIRECTORY on `amazon-bedrock`. Only adapter routes
|
||||
# are per-instance; the configurable-provider directory is global.
|
||||
#
|
||||
# Replacing this row's whole `config` costs nothing: the base mounts it dormant
|
||||
# with no config of its own. A profile that needs to drop or re-aim TokenPlan
|
||||
# restates this row's config rather than disabling an id.
|
||||
- id: llm-pi-ai
|
||||
config:
|
||||
providers:
|
||||
bailian-tokenplan:
|
||||
displayName: Aliyun Bailian TokenPlan
|
||||
api: openai-completions
|
||||
baseURL: https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1
|
||||
# A dedicated name, not DASHSCOPE_API_KEY. TokenPlan keys (sk-sp-)
|
||||
# and pay-as-you-go keys (sk-ws-) are not interchangeable: this
|
||||
# gateway 401s a pay-as-you-go key, and the memory / knowledge-base
|
||||
# endpoints 401 a TokenPlan key. Sharing one variable would make
|
||||
# whichever plugin loses silently fail to authenticate.
|
||||
apiKeyEnv: BAILIAN_TOKENPLAN_API_KEY
|
||||
compat:
|
||||
thinkingFormat: qwen
|
||||
supportsReasoningEffort: true
|
||||
# Verified against GET /compatible-mode/v1/models plus a per-model
|
||||
# image probe on 2026-08-14. Only `id` is required; context window
|
||||
# and max tokens fall back to this provider's defaults.
|
||||
#
|
||||
# `input: [text, image]` is a claim about the endpoint, not a
|
||||
# checked fact, and it is what opens the Web UI paste path and
|
||||
# `read_image`. Every entry carrying it answered a colour question
|
||||
# about a test PNG correctly. The DeepSeek routes deliberately do
|
||||
# NOT carry it: they accept image content without erroring and then
|
||||
# answer "None", so declaring vision would turn a clean refusal
|
||||
# into a silently blind reply. Use `bailian_vision_describe` there.
|
||||
#
|
||||
# Omitted on purpose: wan2.7-image / wan2.7-image-pro are
|
||||
# generation-only (reach them through `bailian_image_generate`) and
|
||||
# qwen-audio-3.0-* are audio endpoints, not chat completions.
|
||||
models:
|
||||
- id: qwen3.8-max
|
||||
input: [text, image]
|
||||
- id: qwen3.7-plus
|
||||
input: [text, image]
|
||||
- id: qwen3.6-flash
|
||||
input: [text, image]
|
||||
# Rejects image content outright with HTTP 400.
|
||||
- id: qwen3.7-max
|
||||
- id: glm-5.2
|
||||
input: [text, image]
|
||||
compat:
|
||||
thinkingFormat: deepseek
|
||||
- id: deepseek-v4-pro
|
||||
compat:
|
||||
thinkingFormat: deepseek
|
||||
- id: deepseek-v4-flash-0731
|
||||
compat:
|
||||
thinkingFormat: deepseek
|
||||
|
||||
- insert:
|
||||
- id: bailian-tool-vision
|
||||
name: bailian-cli-dsh/tool-vision
|
||||
# Client-only row: name is the package ROOT (no subpath) so client-modules
|
||||
# can resolve "<name>/package.json" and detect the dsh.client declaration.
|
||||
# Its node half (dist/index.mjs) is a no-op; the row exists to serve the
|
||||
# browser bundle (client.bundle.js) that renders the Bailian settings page
|
||||
# and the new-session welcome page.
|
||||
- id: bailian-client
|
||||
name: bailian-cli-dsh
|
||||
|
||||
- id: bailian-tool-image
|
||||
name: bailian-cli-dsh/tool-image
|
||||
|
||||
# Enabled by default: the tool creates no resources at load time. It only
|
||||
# provisions a cloud agent when the model actually calls it, and reuses it
|
||||
# after — no deployment-specific ID to configure up front.
|
||||
- id: bailian-tool-managed-agent
|
||||
name: bailian-cli-dsh/tool-managed-agent
|
||||
|
||||
# Disabled by default: the knowledge base to query is deployment-specific,
|
||||
# and an enabled provider with no agentId would make `web_search` ambiguous
|
||||
# for everyone. Set workspaceId + agentId and flip `disabled` to use it.
|
||||
- id: bailian-web-search-rag
|
||||
name: bailian-cli-dsh/web-search-rag
|
||||
disabled: true
|
||||
# TokenPlan usage display (dual-face: Host provides two webServer routes,
|
||||
# Client renders a general "Bailian" settings.section page). All bl commands
|
||||
# use `--config dsh` to isolate credentials in a dedicated bl profile.
|
||||
#
|
||||
# Two routes:
|
||||
# POST /api/bailian/credentials — saves AK/SK to dsh profile
|
||||
# (bl auth login --open-api --config dsh). Generates fresh access_token.
|
||||
# POST /api/bailian/tokenplan/usage — fetches personal-edition usage
|
||||
# using the dsh profile (no AK/SK in body; credentials already saved).
|
||||
#
|
||||
# Users configure AK/SK once on the settings page; all future Bailian
|
||||
# plugins reuse the same dsh profile credentials.
|
||||
#
|
||||
# Config fields:
|
||||
# accessKeyId / accessKeySecret: fallback when not provided via UI.
|
||||
# consoleRegion: default region (cn-beijing).
|
||||
# consoleSite: domestic | international (default: domestic).
|
||||
# profile: bl config profile name (default: dsh).
|
||||
- id: bailian-tokenplan-usage
|
||||
name: bailian-cli-dsh/tokenplan-usage
|
||||
config: {}
|
||||
|
||||
# Disabled by default: memory add/search are billed per call.
|
||||
# Disabled by default: memory add/search are billed per call. Enable in
|
||||
# the profile patch and configure API Key + parameters on the Bailian
|
||||
# settings page. Calls DashScope memory v2 API directly (not bl memory)
|
||||
# for full parameter control (min_score, enable_rerank, plan_version,
|
||||
# memory_library_id, enable_judge, enable_rewrite).
|
||||
- id: bailian-memory
|
||||
name: bailian-cli-dsh/memory
|
||||
disabled: true
|
||||
config: {}
|
||||
config:
|
||||
baseUrl: "https://dashscope.aliyuncs.com/api/v2/apps/memory/"
|
||||
planVersion: "lite"
|
||||
topK: 10
|
||||
autoInject: true
|
||||
autoPersist: true
|
||||
|
||||
+14
-23
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "bailian-cli-dsh",
|
||||
"version": "1.14.2",
|
||||
"description": "Aliyun Model Studio (Bailian) plugin bundle for DeepSeek Harness (dsh): TokenPlan LLM provider, knowledge-base RAG search, vision, image generation, long-term memory, and managed-agent subagents.",
|
||||
"description": "Aliyun Model Studio (Bailian) plugin bundle for DeepSeek Harness (dsh): TokenPlan LLM provider and personal-edition TokenPlan usage display in the webui.",
|
||||
"homepage": "https://bailian.console.aliyun.com/cli",
|
||||
"bugs": {
|
||||
"url": "https://github.com/modelstudioai/cli/issues"
|
||||
@@ -16,6 +16,7 @@
|
||||
"files": [
|
||||
"README.md",
|
||||
"dist",
|
||||
"client.bundle.js",
|
||||
"cordis.patch.yml"
|
||||
],
|
||||
"type": "module",
|
||||
@@ -23,28 +24,17 @@
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
"default": "./dist/index.mjs"
|
||||
},
|
||||
"./tool-vision": {
|
||||
"types": "./src/tool-vision/index.ts",
|
||||
"default": "./src/tool-vision/index.ts"
|
||||
},
|
||||
"./tool-image": {
|
||||
"types": "./src/tool-image/index.ts",
|
||||
"default": "./src/tool-image/index.ts"
|
||||
},
|
||||
"./tool-managed-agent": {
|
||||
"types": "./src/tool-managed-agent/index.ts",
|
||||
"default": "./src/tool-managed-agent/index.ts"
|
||||
},
|
||||
"./web-search-rag": {
|
||||
"types": "./src/web-search-rag/index.ts",
|
||||
"default": "./src/web-search-rag/index.ts"
|
||||
"./tokenplan-usage": {
|
||||
"types": "./src/tokenplan-usage/index.ts",
|
||||
"default": "./dist/tokenplan-usage/index.mjs"
|
||||
},
|
||||
"./memory": {
|
||||
"types": "./src/memory/index.ts",
|
||||
"default": "./src/memory/index.ts"
|
||||
"default": "./dist/memory/index.mjs"
|
||||
},
|
||||
"./client": "./client.bundle.js",
|
||||
"./cordis.patch.yml": "./cordis.patch.yml",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
@@ -52,18 +42,16 @@
|
||||
"access": "public",
|
||||
"exports": {
|
||||
".": "./dist/index.mjs",
|
||||
"./tool-vision": "./dist/tool-vision/index.mjs",
|
||||
"./tool-image": "./dist/tool-image/index.mjs",
|
||||
"./tool-managed-agent": "./dist/tool-managed-agent/index.mjs",
|
||||
"./web-search-rag": "./dist/web-search-rag/index.mjs",
|
||||
"./tokenplan-usage": "./dist/tokenplan-usage/index.mjs",
|
||||
"./memory": "./dist/memory/index.mjs",
|
||||
"./client": "./client.bundle.js",
|
||||
"./cordis.patch.yml": "./cordis.patch.yml",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vp pack",
|
||||
"build": "vp pack && node scripts/build-client.mjs",
|
||||
"dev": "vp pack --watch",
|
||||
"test": "vp test",
|
||||
"check": "vp check"
|
||||
@@ -106,6 +94,9 @@
|
||||
"dsh": {
|
||||
"bundle": {
|
||||
"patch": "./cordis.patch.yml"
|
||||
},
|
||||
"client": {
|
||||
"platform": "web"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Build the browser client bundle in the DSH ModuleLoader closure format.
|
||||
*
|
||||
* The DSH web shell only loads client plugins that call
|
||||
* `window.__ModuleLoader__.load({ id, factory })`, resolving externals (react)
|
||||
* through the injected `require`. vite-plus emits plain ESM (wrong format), so
|
||||
* the client is built separately with esbuild: CJS + browser platform + react
|
||||
* external, wrapped in the ModuleLoader banner/footer.
|
||||
*
|
||||
* Run after `vp pack` (see package.json "build").
|
||||
*/
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
const esbuild = join(pkgDir, "node_modules", ".bin", "esbuild");
|
||||
|
||||
const banner =
|
||||
'window.__ModuleLoader__.load({ id: "bailian-cli-dsh", factory: (require) => { ' +
|
||||
"var module = { exports: {} }; var exports = module.exports;";
|
||||
const footer = "return module.exports; } });";
|
||||
|
||||
const result = spawnSync(
|
||||
esbuild,
|
||||
[
|
||||
"src/client.ts",
|
||||
"--bundle",
|
||||
"--format=cjs",
|
||||
"--platform=browser",
|
||||
"--external:react",
|
||||
`--banner:js=${banner}`,
|
||||
`--footer:js=${footer}`,
|
||||
"--outfile=client.bundle.js",
|
||||
],
|
||||
{ cwd: pkgDir, stdio: "inherit" },
|
||||
);
|
||||
|
||||
if (result.status !== 0) {
|
||||
// Throw rather than process.exit: an uncaught top-level error still yields a
|
||||
// non-zero exit (so `pnpm build` fails), and it carries esbuild's own status.
|
||||
throw new Error(`build-client: esbuild failed with status ${result.status ?? "unknown"}`);
|
||||
}
|
||||
console.log("build-client: client.bundle.js (ModuleLoader format) written");
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* Bailian feature registry — single source of truth mapping a welcome-page
|
||||
* card to a **bailian-cli command**. The console-API knowledge lives in
|
||||
* bailian-cli (packages/commands); this bundle only shells out to `bl`, so a
|
||||
* feature added there is reusable here for free.
|
||||
*
|
||||
* Each entry is exposed two ways by the Host:
|
||||
* 1. a **model tool** `bailian_<id>` (natural-language entry: the LLM reads
|
||||
* `intent` and calls the tool when the user asks in plain language);
|
||||
* 2. the **generic route** `POST /bailian/console { featureId }` (card-click
|
||||
* entry: the client renders `summarize`/`data`).
|
||||
*
|
||||
* Adding a feature = (a) add a `bl` command in bailian-cli, (b) add one record
|
||||
* here. Tool + card come for free.
|
||||
*
|
||||
* Browser-safe (no node imports) so both the vite host build and the esbuild
|
||||
* client bundle can import it.
|
||||
*
|
||||
* @module bailian-cli-dsh/features
|
||||
*/
|
||||
|
||||
export interface BailianFeature {
|
||||
/** Stable id; tool name is `bailian_<id>`. */
|
||||
id: string;
|
||||
/** Card title (matched against welcome cards). */
|
||||
title: string;
|
||||
/** Card description. */
|
||||
desc: string;
|
||||
/** Tool description: tells the LLM which user utterances should use it. */
|
||||
intent: string;
|
||||
/** Natural-language query sent into the conversation when the card is clicked. */
|
||||
query: string;
|
||||
/** `bl` command args (without `--output`); the Host appends `--output json`. */
|
||||
argv: string[];
|
||||
/** Args appended when the user supplies no params (e.g. ["--all"]). */
|
||||
defaultArgs?: string[];
|
||||
/** Optional params the LLM (or UI) may supply; mapped to bl flags. */
|
||||
paramFlags?: FeatureParam[];
|
||||
/** Human/LLM summary of the command's JSON output. */
|
||||
summarize: (data: any) => string;
|
||||
}
|
||||
|
||||
export interface FeatureParam {
|
||||
/** Tool parameter name (LLM fills it). */
|
||||
name: string;
|
||||
/** bl flag it maps to (e.g. --model). */
|
||||
flag: string;
|
||||
type: "string" | "number" | "boolean";
|
||||
description: string;
|
||||
}
|
||||
|
||||
function pick(obj: any, ...keys: string[]): any {
|
||||
for (const k of keys) if (obj && obj[k] !== undefined && obj[k] !== null) return obj[k];
|
||||
return undefined;
|
||||
}
|
||||
function pct(v: any): string {
|
||||
if (v === undefined || v === null) return "—";
|
||||
const n = (typeof v === "number" ? v : Number(v)) * 100;
|
||||
return (isNaN(n) ? 0 : n).toFixed(1) + "%";
|
||||
}
|
||||
|
||||
export const FEATURES: BailianFeature[] = [
|
||||
{
|
||||
id: "free-tier",
|
||||
title: "免费额度一键防护",
|
||||
desc: "查询免费额度用量,一键开启「用完即停」,额度耗尽自动停止调用,不再产生意外扣费",
|
||||
intent:
|
||||
"查询百炼免费额度用量与『用完即停』防护状态。当用户提到免费额度、额度耗尽、意外扣费、用完即停、额度防护时使用。",
|
||||
query: "帮我查看百炼免费额度用量,并告诉我怎么开启「用完即停」防护",
|
||||
argv: ["usage", "freetier"],
|
||||
defaultArgs: ["--all"],
|
||||
paramFlags: [
|
||||
{
|
||||
name: "models",
|
||||
flag: "--model",
|
||||
type: "string",
|
||||
description:
|
||||
"逗号分隔的模型列表;不填则查询全部(--all)。若用户只关心特定模型且未说明,可先用 AskUserQuestion 询问。",
|
||||
},
|
||||
],
|
||||
summarize: (d) => {
|
||||
if (!d || typeof d !== "object") return "未获取到免费额度数据。";
|
||||
const list = pick(d, "quotas", "quotaList", "models", "list");
|
||||
if (Array.isArray(list)) {
|
||||
const lines = list.slice(0, 8).map((m: any) => {
|
||||
const model = pick(m, "model", "modelName", "modelId") ?? "?";
|
||||
const total = pick(m, "quotaTotal", "totalQuota", "total");
|
||||
const used = pick(m, "quotaUsed", "usedQuota", "used");
|
||||
const on = pick(m, "freeTierOnly");
|
||||
return `- ${model}: 已用 ${used ?? "?"} / 共 ${total ?? "?"}${on !== undefined ? `,用完即停 ${on ? "开" : "关"}` : ""}`;
|
||||
});
|
||||
return lines.length
|
||||
? `免费额度:\n${lines.join("\n")}`
|
||||
: "免费额度: " + JSON.stringify(d).slice(0, 300);
|
||||
}
|
||||
return "免费额度: " + JSON.stringify(d).slice(0, 300);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "usage",
|
||||
title: "模型用量统计",
|
||||
desc: "各模型/TokenPlan 的用量与百分比一次查清,自动生成用量分析",
|
||||
intent:
|
||||
"查询百炼 TokenPlan 个人版用量(5 小时/1 周窗口百分比、重置时间、套餐、用量包)。当用户问用量、用了多少、额度百分比、TokenPlan 使用情况时使用。",
|
||||
query: "帮我查询百炼 TokenPlan 个人版用量(5 小时/1 周窗口、套餐与用量包)",
|
||||
argv: ["token-plan", "personal-usage"],
|
||||
summarize: (d) => {
|
||||
if (!d || typeof d !== "object") return "未获取到用量数据。";
|
||||
const u = d.usage ?? d;
|
||||
const parts: string[] = [];
|
||||
if (u.per5HourPercentage !== undefined)
|
||||
parts.push(`5 小时窗口已用 ${pct(u.per5HourPercentage)}`);
|
||||
if (u.per1WeekPercentage !== undefined)
|
||||
parts.push(`1 周窗口已用 ${pct(u.per1WeekPercentage)}`);
|
||||
const sub = d.subscription;
|
||||
if (sub && sub.remainingDays !== undefined) parts.push(`套餐剩余 ${sub.remainingDays} 天`);
|
||||
const add = d.addonSummary;
|
||||
if (add && add.remainingCredits !== undefined)
|
||||
parts.push(`用量包剩余 ${add.remainingCredits}/${add.totalCredits}`);
|
||||
return parts.length
|
||||
? `TokenPlan 用量: ${parts.join(";")}`
|
||||
: "用量: " + JSON.stringify(d).slice(0, 300);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function featureById(id: string): BailianFeature | undefined {
|
||||
return FEATURES.find((f) => f.id === id);
|
||||
}
|
||||
export function featureByTitle(title: string): BailianFeature | undefined {
|
||||
return FEATURES.find((f) => f.title === title);
|
||||
}
|
||||
@@ -1,9 +1,20 @@
|
||||
/**
|
||||
* bailian-cli-dsh — Aliyun Model Studio capabilities as a DeepSeek Harness
|
||||
* profile bundle. The package's substance is `cordis.patch.yml`, declared by
|
||||
* the `dsh.bundle.patch` manifest field and resolved by the profile composer
|
||||
* through that field; this module carries no runtime API.
|
||||
* the `dsh.bundle.patch` manifest field and resolved by the profile composer.
|
||||
*
|
||||
* This root module is the no-op node half loaded by the `bailian-client` row
|
||||
* (whose purpose is to make `client-modules` serve the browser bundle
|
||||
* `client.bundle.js`). Cordis requires every row to resolve to a plugin with
|
||||
* an `apply` method, so this exports a minimal one. The real Host logic lives
|
||||
* in `./tokenplan-usage` and `./memory`; the browser UI lives in
|
||||
* `client.bundle.js`.
|
||||
*
|
||||
* @module bailian-cli-dsh
|
||||
*/
|
||||
|
||||
export {};
|
||||
/** Cordis plugin name used by loader diagnostics. */
|
||||
export const name = "bailian-cli-dsh";
|
||||
|
||||
/** No-op: this row exists only to serve the client bundle. */
|
||||
export function apply(): void {}
|
||||
|
||||
+353
-101
@@ -1,20 +1,25 @@
|
||||
/**
|
||||
* `bailian-cli-dsh/memory`: cross-session long-term memory backed by Bailian's
|
||||
* hosted memory library.
|
||||
* `bailian-cli-dsh/memory` (Host half): cross-session long-term memory backed
|
||||
* by Bailian's hosted memory library (DashScope memory v2 API).
|
||||
*
|
||||
* dsh has no memory seam — `ctx.compaction` only summarizes within one
|
||||
* session's context window and never writes across sessions — so this plugin
|
||||
* supplies the whole capability: two tools for deliberate reads and writes,
|
||||
* plus automatic retrieval and persistence around each turn.
|
||||
* Provides:
|
||||
* - Two model tools: `bailian_memory_search` (recall) + `bailian_memory_add`
|
||||
* (store), plus `bailian_memory_list` (browse).
|
||||
* - Auto-inject: on the first turn of each session (or every turn if
|
||||
* configured), search memory and inject relevant facts into context.
|
||||
* - Auto-persist: when a turn closes, send new user/assistant messages to
|
||||
* the add API so future sessions can recall them.
|
||||
* - webServer routes for the Client settings page to configure memory
|
||||
* parameters (apiKey, baseUrl, userId, planVersion, etc.).
|
||||
*
|
||||
* Calls go straight to DashScope rather than through `bl memory`, because the
|
||||
* v2 API exposes retrieval controls (`min_score`, `plan_version`,
|
||||
* `enable_rerank`, `meta_data`) the CLI does not surface.
|
||||
* Calls go straight to DashScope rather than through `bl memory`, because
|
||||
* the v2 API exposes retrieval controls (`min_score`, `plan_version`,
|
||||
* `enable_rerank`, `memory_library_id`, `enable_judge`, `enable_rewrite`)
|
||||
* the CLI does not surface.
|
||||
*
|
||||
* BILLING: add and search are charged per call, and `pro` costs roughly fifty
|
||||
* times `lite` per search. Automatic behaviour therefore defaults to `lite`,
|
||||
* retrieves once per session rather than once per turn, and never requests
|
||||
* profile extraction unless a schema is configured.
|
||||
* BILLING: add and search are charged per call. `pro` costs ~50x `lite` per
|
||||
* search. The `enable_rerank` flag is what actually selects the billing tier
|
||||
* (verified: sending `plan_version: lite` alone still bills `pro`).
|
||||
*
|
||||
* @module bailian-cli-dsh/memory
|
||||
*/
|
||||
@@ -26,45 +31,49 @@ import type { ContentBlock, Message } from "@deepseek-ai/dsh-llm";
|
||||
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
||||
import { defineTool } from "@deepseek-ai/dsh-tools";
|
||||
import z from "@deepseek-ai/schemastery";
|
||||
import { dashScopeFetch, resolveApiKey, resolveBaseUrl } from "../shared/http.ts";
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { isTokenPlanKey, tokenPlanKeyRejection } from "../shared/credentials.ts";
|
||||
import { dashScopeFetch } from "../shared/http.ts";
|
||||
|
||||
/** Cordis plugin name used by loader diagnostics. */
|
||||
export const name = "bailian-memory";
|
||||
|
||||
/** Seams this plugin registers into. */
|
||||
export const inject = ["tools", "agents"];
|
||||
export const inject = ["tools", "agents", "webServer"];
|
||||
|
||||
export interface Config {
|
||||
/** DashScope API key (pay-as-you-go sk-ws-). Falls back to $DASHSCOPE_API_KEY. */
|
||||
apiKey?: string;
|
||||
/** Memory API base URL (default: https://dashscope.aliyuncs.com/api/v2/apps/memory/). */
|
||||
baseUrl?: string;
|
||||
/** Memory entity id. Falls back to `$BAILIAN_MEMORY_USER_ID`, then the OS user. */
|
||||
/** Memory entity id. Falls back to $BAILIAN_MEMORY_USER_ID, then OS user. */
|
||||
userId?: string;
|
||||
/** Memory library id; defaults to the account default. */
|
||||
memoryLibraryId?: string;
|
||||
/** Memory extraction rule id. */
|
||||
projectId?: string;
|
||||
/** Profile template id; omitting it skips profile extraction (and its cost). */
|
||||
/** Profile template id; omitting skips profile extraction (and its cost). */
|
||||
profileSchema?: string;
|
||||
/** `lite` disables rerank and is ~50x cheaper per search. */
|
||||
/** Search strategy; pro enables rerank at ~50x the cost. */
|
||||
planVersion?: "lite" | "pro";
|
||||
topK?: number;
|
||||
minScore?: number;
|
||||
/** Retrieve relevant memories and inject them into the conversation. */
|
||||
/** Retrieve relevant memories and inject into the conversation. */
|
||||
autoInject?: boolean;
|
||||
/** Retrieve on every turn instead of once per session. Costs one search per turn. */
|
||||
/** Retrieve every turn instead of once per session. */
|
||||
injectEveryTurn?: boolean;
|
||||
/** Persist each turn's new messages when the turn closes. */
|
||||
autoPersist?: boolean;
|
||||
}
|
||||
|
||||
export const Config: z<Config> = z.object({
|
||||
apiKey: z.string().role("secret").description("DashScope key; defaults to $DASHSCOPE_API_KEY."),
|
||||
baseUrl: z.string().description("DashScope base URL override."),
|
||||
apiKey: z.string().role("secret").description("Pay-as-you-go DashScope API key (sk-)."),
|
||||
baseUrl: z.string().description("Memory API base URL."),
|
||||
userId: z.string().description("Memory entity id owning these memories."),
|
||||
memoryLibraryId: z.string().description("Memory library id; defaults to the account default."),
|
||||
memoryLibraryId: z.string().description("Memory library id."),
|
||||
projectId: z.string().description("Memory extraction rule id."),
|
||||
profileSchema: z.string().description("Profile template id; enables profile extraction."),
|
||||
planVersion: z
|
||||
.union(["lite", "pro"] as const)
|
||||
.description("Search strategy; pro enables rerank at ~50x the cost."),
|
||||
planVersion: z.union(["lite", "pro"] as const).description("Search strategy; pro ~50x cost."),
|
||||
topK: z.natural().description("Maximum memories to recall (1-100)."),
|
||||
minScore: z.number().description("Minimum similarity score, 0-1."),
|
||||
autoInject: z.boolean().description("Inject recalled memories automatically."),
|
||||
@@ -72,9 +81,12 @@ export const Config: z<Config> = z.object({
|
||||
autoPersist: z.boolean().description("Persist new messages when a turn closes."),
|
||||
});
|
||||
|
||||
const DEFAULT_BASE_URL = "https://dashscope.aliyuncs.com/api/v2/apps/memory/";
|
||||
const DEFAULT_TOP_K = 10;
|
||||
const DEFAULT_PLAN_VERSION = "lite";
|
||||
const MEMORY_PATH = "/api/v2/apps/memory";
|
||||
|
||||
const CONFIG_ROUTE = "/bailian/memory/config";
|
||||
const STATUS_ROUTE = "/bailian/memory/status";
|
||||
|
||||
interface MemoryNode {
|
||||
memory_node_id?: string;
|
||||
@@ -83,11 +95,16 @@ interface MemoryNode {
|
||||
old_content?: string;
|
||||
created_at?: number;
|
||||
updated_at?: number;
|
||||
meta_data?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface MemoryResponse {
|
||||
request_id?: string;
|
||||
memory_nodes?: readonly MemoryNode[];
|
||||
total?: number;
|
||||
page_num?: number;
|
||||
page_size?: number;
|
||||
billing_plan?: string;
|
||||
}
|
||||
|
||||
interface ChatTurn {
|
||||
@@ -95,7 +112,23 @@ interface ChatTurn {
|
||||
content: string;
|
||||
}
|
||||
|
||||
/** Resolution order: explicit config, then environment, then the OS user. */
|
||||
/** Mutable runtime config — updated via webServer route, initialized from Cordis config. */
|
||||
interface MemoryRuntimeConfig {
|
||||
apiKey: string | undefined;
|
||||
baseUrl: string;
|
||||
userId: string;
|
||||
memoryLibraryId: string | undefined;
|
||||
projectId: string | undefined;
|
||||
profileSchema: string | undefined;
|
||||
planVersion: "lite" | "pro";
|
||||
topK: number;
|
||||
minScore: number | undefined;
|
||||
autoInject: boolean;
|
||||
injectEveryTurn: boolean;
|
||||
autoPersist: boolean;
|
||||
}
|
||||
|
||||
/** Resolution order: explicit config, then env, then OS user. */
|
||||
function resolveUserId(ctx: Context, config: Config): string {
|
||||
if (config.userId !== undefined && config.userId.length > 0) return config.userId;
|
||||
const fromEnv = ctx.get("launchEnvironment")?.get("BAILIAN_MEMORY_USER_ID")?.value;
|
||||
@@ -123,19 +156,52 @@ function conversationTurns(messages: readonly Message[]): ChatTurn[] {
|
||||
return turns;
|
||||
}
|
||||
|
||||
/** Read a UTF-8 POST body up to a size limit. */
|
||||
function readJsonBody(req: IncomingMessage, maxBytes: number = 16384): Promise<unknown> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
let total = 0;
|
||||
req.on("data", (chunk: Buffer) => {
|
||||
total += chunk.length;
|
||||
if (total > maxBytes) {
|
||||
req.destroy();
|
||||
reject(new Error("body too large"));
|
||||
return;
|
||||
}
|
||||
chunks.push(chunk);
|
||||
});
|
||||
req.on("end", () => {
|
||||
const text = Buffer.concat(chunks).toString("utf8");
|
||||
if (text.length === 0) return resolve({});
|
||||
try {
|
||||
resolve(JSON.parse(text));
|
||||
} catch {
|
||||
reject(new Error("invalid JSON"));
|
||||
}
|
||||
});
|
||||
req.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function sendJson(res: ServerResponse, status: number, data: unknown): void {
|
||||
res.statusCode = status;
|
||||
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
||||
res.end(JSON.stringify(data));
|
||||
}
|
||||
|
||||
class MemoryClient {
|
||||
constructor(
|
||||
private readonly apiKey: string,
|
||||
private readonly baseUrl: string,
|
||||
private readonly config: Config,
|
||||
private readonly cfg: MemoryRuntimeConfig,
|
||||
private readonly userId: string,
|
||||
) {}
|
||||
|
||||
private shared(): Record<string, unknown> {
|
||||
return {
|
||||
user_id: this.userId,
|
||||
...(this.config.memoryLibraryId !== undefined
|
||||
? { memory_library_id: this.config.memoryLibraryId }
|
||||
...(this.cfg.memoryLibraryId !== undefined
|
||||
? { memory_library_id: this.cfg.memoryLibraryId }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
@@ -146,7 +212,7 @@ class MemoryClient {
|
||||
overrides?: { customContent?: string; metaData?: Record<string, unknown> },
|
||||
): Promise<MemoryResponse> {
|
||||
return dashScopeFetch<MemoryResponse>({
|
||||
url: `${this.baseUrl}${MEMORY_PATH}/add`,
|
||||
url: `${this.baseUrl}add`,
|
||||
method: "POST",
|
||||
apiKey: this.apiKey,
|
||||
signal,
|
||||
@@ -155,10 +221,8 @@ class MemoryClient {
|
||||
...(overrides?.customContent !== undefined
|
||||
? { custom_content: overrides.customContent }
|
||||
: { messages }),
|
||||
...(this.config.projectId !== undefined ? { project_id: this.config.projectId } : {}),
|
||||
...(this.config.profileSchema !== undefined
|
||||
? { profile_schema: this.config.profileSchema }
|
||||
: {}),
|
||||
...(this.cfg.projectId !== undefined ? { project_id: this.cfg.projectId } : {}),
|
||||
...(this.cfg.profileSchema !== undefined ? { profile_schema: this.cfg.profileSchema } : {}),
|
||||
...(overrides?.metaData !== undefined ? { meta_data: overrides.metaData } : {}),
|
||||
},
|
||||
});
|
||||
@@ -169,47 +233,64 @@ class MemoryClient {
|
||||
signal: AbortSignal | undefined,
|
||||
overrides?: { topK?: number; minScore?: number; planVersion?: "lite" | "pro" },
|
||||
): Promise<MemoryResponse> {
|
||||
const planVersion = overrides?.planVersion ?? this.config.planVersion ?? DEFAULT_PLAN_VERSION;
|
||||
const planVersion = overrides?.planVersion ?? this.cfg.planVersion ?? DEFAULT_PLAN_VERSION;
|
||||
return dashScopeFetch<MemoryResponse>({
|
||||
url: `${this.baseUrl}${MEMORY_PATH}/memory_nodes/search`,
|
||||
url: `${this.baseUrl}memory_nodes/search`,
|
||||
method: "POST",
|
||||
apiKey: this.apiKey,
|
||||
signal,
|
||||
body: {
|
||||
...this.shared(),
|
||||
messages,
|
||||
top_k: overrides?.topK ?? this.config.topK ?? DEFAULT_TOP_K,
|
||||
...((overrides?.minScore ?? this.config.minScore) !== undefined
|
||||
? { min_score: overrides?.minScore ?? this.config.minScore }
|
||||
top_k: overrides?.topK ?? this.cfg.topK ?? DEFAULT_TOP_K,
|
||||
...((overrides?.minScore ?? this.cfg.minScore) !== undefined
|
||||
? { min_score: overrides?.minScore ?? this.cfg.minScore }
|
||||
: {}),
|
||||
// `enable_rerank` is what actually selects the billing tier. Sending
|
||||
// `plan_version: lite` alone still bills `pro` (verified against the
|
||||
// live API), despite the documented precedence, and pro costs ~50x
|
||||
// more per search. Send both: the flag that works, plus the
|
||||
// documented field in case the server-side precedence is fixed.
|
||||
// enable_rerank is what actually selects the billing tier (verified:
|
||||
// plan_version alone still bills pro). Send both for safety.
|
||||
enable_rerank: planVersion === "pro",
|
||||
plan_version: planVersion,
|
||||
...(this.config.projectId !== undefined ? { project_ids: [this.config.projectId] } : {}),
|
||||
...(this.cfg.projectId !== undefined ? { project_ids: [this.cfg.projectId] } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async list(
|
||||
signal: AbortSignal | undefined,
|
||||
overrides?: { pageNum?: number; pageSize?: number },
|
||||
): Promise<MemoryResponse> {
|
||||
const params = new URLSearchParams({
|
||||
user_id: this.userId,
|
||||
page_num: String(overrides?.pageNum ?? 1),
|
||||
page_size: String(overrides?.pageSize ?? 10),
|
||||
...(this.cfg.memoryLibraryId !== undefined
|
||||
? { memory_library_id: this.cfg.memoryLibraryId }
|
||||
: {}),
|
||||
});
|
||||
return dashScopeFetch<MemoryResponse>({
|
||||
url: `${this.baseUrl}memory_nodes?${params.toString()}`,
|
||||
method: "GET",
|
||||
apiKey: this.apiKey,
|
||||
signal,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function renderMemories(nodes: readonly MemoryNode[]): string {
|
||||
const lines = nodes
|
||||
function formatMemories(nodes: readonly MemoryNode[]): string {
|
||||
const items = nodes
|
||||
.map((node) => node.content?.trim())
|
||||
.filter((content): content is string => content !== undefined && content.length > 0)
|
||||
.map((content) => `- ${content}`);
|
||||
return `What you remember about this user from earlier sessions:\n${lines.join("\n")}`;
|
||||
.filter((content): content is string => content !== undefined && content.length > 0);
|
||||
if (items.length === 0) return "";
|
||||
return `What you remember about this user from earlier sessions:\n${items.map((item) => `- ${item}`).join("\n")}`;
|
||||
}
|
||||
|
||||
function registerTools(ctx: Context, client: MemoryClient): void {
|
||||
/** Register model tools for deliberate memory operations. */
|
||||
function registerTools(ctx: Context, client: () => MemoryClient | undefined): void {
|
||||
ctx.tools.register(
|
||||
defineTool({
|
||||
name: "bailian_memory_search",
|
||||
description:
|
||||
"Recall facts stored about this user in earlier sessions. Use when the user refers " +
|
||||
"to prior context, preferences, or decisions you have no record of in this session.",
|
||||
"Recall facts stored about this user in earlier sessions. Use when the user refers to prior context, preferences, or decisions you have no record of in this session.",
|
||||
parameters: {
|
||||
query: { type: "string", required: true, description: "What to recall." },
|
||||
top_k: { type: "integer", description: "Maximum memories to return (1-100)." },
|
||||
@@ -240,18 +321,23 @@ function registerTools(ctx: Context, client: MemoryClient): void {
|
||||
text:
|
||||
value.memories.length === 0
|
||||
? "No relevant memories."
|
||||
: value.memories.map((memory) => `- ${memory.content}`).join("\n"),
|
||||
: value.memories.map((m: any) => `- ${m.content}`).join("\n"),
|
||||
},
|
||||
],
|
||||
},
|
||||
isConcurrencySafe: () => true,
|
||||
async execute(args, exec) {
|
||||
const response = await client.search([{ role: "user", content: args.query }], exec.signal, {
|
||||
const mem = client();
|
||||
if (mem === undefined)
|
||||
throw new Error(
|
||||
"bailian-memory: not configured. Set apiKey in the Bailian settings page or config.",
|
||||
);
|
||||
const result = await mem.search([{ role: "user", content: args.query }], exec.signal, {
|
||||
...(args.top_k !== undefined ? { topK: args.top_k } : {}),
|
||||
...(args.min_score !== undefined ? { minScore: args.min_score } : {}),
|
||||
});
|
||||
return {
|
||||
memories: (response.memory_nodes ?? []).map((node) => ({
|
||||
memories: (result.memory_nodes ?? []).map((node) => ({
|
||||
id: node.memory_node_id ?? "",
|
||||
content: node.content ?? "",
|
||||
})),
|
||||
@@ -264,8 +350,7 @@ function registerTools(ctx: Context, client: MemoryClient): void {
|
||||
defineTool({
|
||||
name: "bailian_memory_add",
|
||||
description:
|
||||
"Store a durable fact about this user so later sessions can recall it. Use for stable " +
|
||||
"preferences, decisions, and context — not for transient task state.",
|
||||
"Store a durable fact about this user so later sessions can recall it. Use for stable preferences, decisions, and context — not for transient task state.",
|
||||
parameters: {
|
||||
content: { type: "string", required: true, description: "The fact to remember." },
|
||||
},
|
||||
@@ -280,40 +365,117 @@ function registerTools(ctx: Context, client: MemoryClient): void {
|
||||
],
|
||||
},
|
||||
async execute(args, exec) {
|
||||
const response = await client.add([], exec.signal, { customContent: args.content });
|
||||
return { stored: (response.memory_nodes ?? []).length };
|
||||
const mem = client();
|
||||
if (mem === undefined)
|
||||
throw new Error(
|
||||
"bailian-memory: not configured. Set apiKey in the Bailian settings page or config.",
|
||||
);
|
||||
const result = await mem.add([], exec.signal, { customContent: args.content });
|
||||
return { stored: (result.memory_nodes ?? []).length };
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
ctx.tools.register(
|
||||
defineTool({
|
||||
name: "bailian_memory_list",
|
||||
description:
|
||||
"List all stored memory fragments for this user. Use to review what the system already knows.",
|
||||
parameters: {
|
||||
page_size: { type: "integer", description: "Results per page (default 10)." },
|
||||
page_num: { type: "integer", description: "Page number, starting from 1." },
|
||||
},
|
||||
output: {
|
||||
schema: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
memories: {
|
||||
type: "array",
|
||||
required: true,
|
||||
items: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
id: { type: "string", required: true },
|
||||
content: { type: "string", required: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
total: { type: "integer", required: true },
|
||||
},
|
||||
},
|
||||
render: (_args, value) => [
|
||||
{
|
||||
type: "text",
|
||||
text: `${value.total} memory fragment(s):\n${value.memories.map((m: any) => `- ${m.content}`).join("\n")}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
isConcurrencySafe: () => true,
|
||||
async execute(args, exec) {
|
||||
const mem = client();
|
||||
if (mem === undefined) throw new Error("bailian-memory: not configured.");
|
||||
const result = await mem.list(exec.signal, {
|
||||
...(args.page_size !== undefined ? { pageSize: args.page_size } : {}),
|
||||
...(args.page_num !== undefined ? { pageNum: args.page_num } : {}),
|
||||
});
|
||||
return {
|
||||
memories: (result.memory_nodes ?? []).map((node) => ({
|
||||
id: node.memory_node_id ?? "",
|
||||
content: node.content ?? "",
|
||||
})),
|
||||
total: result.total ?? 0,
|
||||
};
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function registerLifecycle(ctx: Context, client: MemoryClient, config: Config): void {
|
||||
const injected = new WeakSet<Agent>();
|
||||
const persistedUpTo = new WeakMap<Agent, number>();
|
||||
/** Auto-inject (search on first turn) + auto-persist (add on turn end). */
|
||||
function registerAutoBehavior(
|
||||
ctx: Context,
|
||||
client: () => MemoryClient | undefined,
|
||||
cfg: () => MemoryRuntimeConfig,
|
||||
): void {
|
||||
const injectedSessions = new WeakSet<Agent>();
|
||||
const persistedCursor = new WeakMap<Agent, number>();
|
||||
|
||||
if (config.autoInject !== false) {
|
||||
const currentCfg = cfg();
|
||||
|
||||
if (currentCfg.autoInject !== false) {
|
||||
ctx.on(
|
||||
"agent/pre-step",
|
||||
async ({ agent, messages, signal }, next): Promise<PreStepDecision> => {
|
||||
async (
|
||||
{
|
||||
agent,
|
||||
messages,
|
||||
signal,
|
||||
}: { agent: Agent; messages: readonly Message[]; signal: AbortSignal },
|
||||
next: () => Promise<PreStepDecision>,
|
||||
) => {
|
||||
const decision = await next();
|
||||
if (decision.kind !== "enter") return decision;
|
||||
if (injected.has(agent) && config.injectEveryTurn !== true) return decision;
|
||||
if (injectedSessions.has(agent) && currentCfg.injectEveryTurn !== true) return decision;
|
||||
|
||||
const query = textOf(messages.flatMap((message) => message.content));
|
||||
const query = textOf(messages.flatMap((m) => m.content));
|
||||
if (query.length === 0) return decision;
|
||||
|
||||
let nodes: readonly MemoryNode[];
|
||||
const mem = client();
|
||||
if (mem === undefined) return decision;
|
||||
|
||||
let nodes: readonly MemoryNode[] = [];
|
||||
try {
|
||||
const response = await client.search([{ role: "user", content: query }], signal);
|
||||
nodes = response.memory_nodes ?? [];
|
||||
const result = await mem.search([{ role: "user", content: query }], signal);
|
||||
nodes = result.memory_nodes ?? [];
|
||||
} catch {
|
||||
// Recall is an enhancement; a memory-service outage must not stop the turn.
|
||||
return decision;
|
||||
}
|
||||
injected.add(agent);
|
||||
|
||||
injectedSessions.add(agent);
|
||||
if (nodes.length === 0) return decision;
|
||||
|
||||
const text = renderMemories(nodes);
|
||||
const text = formatMemories(nodes);
|
||||
return {
|
||||
...decision,
|
||||
messages: [
|
||||
@@ -334,36 +496,126 @@ function registerLifecycle(ctx: Context, client: MemoryClient, config: Config):
|
||||
);
|
||||
}
|
||||
|
||||
if (config.autoPersist !== false) {
|
||||
ctx.on("agent/turn-stopping", async ({ agent, signal }): Promise<void> => {
|
||||
const turns = conversationTurns(agent.session.deriveMessages());
|
||||
const from = persistedUpTo.get(agent) ?? 0;
|
||||
const fresh = turns.slice(from);
|
||||
if (fresh.length === 0) return;
|
||||
persistedUpTo.set(agent, turns.length);
|
||||
try {
|
||||
await client.add(fresh, signal);
|
||||
} catch {
|
||||
// Persistence is best-effort; never fail a turn over it.
|
||||
persistedUpTo.set(agent, from);
|
||||
}
|
||||
});
|
||||
if (currentCfg.autoPersist !== false) {
|
||||
ctx.on(
|
||||
"agent/turn-stopping",
|
||||
async ({ agent, signal }: { agent: Agent; signal: AbortSignal }) => {
|
||||
const mem = client();
|
||||
if (mem === undefined) return;
|
||||
|
||||
const turns = conversationTurns(agent.session.deriveMessages());
|
||||
const cursor = persistedCursor.get(agent) ?? 0;
|
||||
const newTurns = turns.slice(cursor);
|
||||
if (newTurns.length === 0) return;
|
||||
|
||||
persistedCursor.set(agent, turns.length);
|
||||
try {
|
||||
await mem.add(newTurns, signal);
|
||||
} catch {
|
||||
persistedCursor.set(agent, cursor);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
const apiKey = resolveApiKey(ctx, config.apiKey);
|
||||
if (apiKey === undefined) {
|
||||
throw new Error(
|
||||
"bailian-memory: no DashScope API key. Set $DASHSCOPE_API_KEY or configure `apiKey`.",
|
||||
const webServer = ctx.get("webServer");
|
||||
|
||||
// Mutable runtime config — initialized from Cordis config, updatable via webServer route.
|
||||
let runtime: MemoryRuntimeConfig = {
|
||||
apiKey: config.apiKey,
|
||||
baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
|
||||
userId: resolveUserId(ctx, config),
|
||||
memoryLibraryId: config.memoryLibraryId,
|
||||
projectId: config.projectId,
|
||||
profileSchema: config.profileSchema,
|
||||
planVersion: config.planVersion ?? DEFAULT_PLAN_VERSION,
|
||||
topK: config.topK ?? DEFAULT_TOP_K,
|
||||
minScore: config.minScore,
|
||||
autoInject: config.autoInject ?? true,
|
||||
injectEveryTurn: config.injectEveryTurn ?? false,
|
||||
autoPersist: config.autoPersist ?? true,
|
||||
};
|
||||
|
||||
/** Build a MemoryClient from the current runtime config, or undefined if no API key. */
|
||||
function buildClient(): MemoryClient | undefined {
|
||||
if (runtime.apiKey === undefined || runtime.apiKey.length === 0) return undefined;
|
||||
if (isTokenPlanKey(runtime.apiKey)) {
|
||||
throw new Error(tokenPlanKeyRejection(name, "the memory API"));
|
||||
}
|
||||
return new MemoryClient(runtime.apiKey, runtime.baseUrl, runtime, runtime.userId);
|
||||
}
|
||||
|
||||
// Register tools + auto behavior.
|
||||
registerTools(ctx, buildClient);
|
||||
registerAutoBehavior(ctx, buildClient, () => runtime);
|
||||
|
||||
// webServer routes for the Client settings page.
|
||||
if (webServer !== undefined) {
|
||||
ctx.effect(() =>
|
||||
webServer.register({
|
||||
kind: "exact",
|
||||
path: STATUS_ROUTE,
|
||||
handler: async (_req: IncomingMessage, res: ServerResponse) => {
|
||||
sendJson(res, 200, {
|
||||
configured: runtime.apiKey !== undefined && runtime.apiKey.length > 0,
|
||||
userId: runtime.userId,
|
||||
baseUrl: runtime.baseUrl,
|
||||
planVersion: runtime.planVersion,
|
||||
topK: runtime.topK,
|
||||
autoInject: runtime.autoInject,
|
||||
injectEveryTurn: runtime.injectEveryTurn,
|
||||
autoPersist: runtime.autoPersist,
|
||||
memoryLibraryId: runtime.memoryLibraryId,
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
ctx.effect(() =>
|
||||
webServer.register({
|
||||
kind: "exact",
|
||||
path: CONFIG_ROUTE,
|
||||
handler: async (req: IncomingMessage, res: ServerResponse) => {
|
||||
if (req.method !== "POST") {
|
||||
sendJson(res, 405, { error: "use POST" });
|
||||
return;
|
||||
}
|
||||
let body: Record<string, unknown>;
|
||||
try {
|
||||
body = (await readJsonBody(req)) as Record<string, unknown>;
|
||||
} catch (error) {
|
||||
sendJson(res, 400, { error: error instanceof Error ? error.message : "bad request" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Update mutable fields from the request body.
|
||||
if (typeof body.apiKey === "string") runtime.apiKey = body.apiKey || undefined;
|
||||
if (typeof body.baseUrl === "string" && body.baseUrl.length > 0)
|
||||
runtime.baseUrl = body.baseUrl;
|
||||
if (typeof body.userId === "string" && body.userId.length > 0)
|
||||
runtime.userId = body.userId;
|
||||
if (typeof body.memoryLibraryId === "string")
|
||||
runtime.memoryLibraryId = body.memoryLibraryId || undefined;
|
||||
if (typeof body.projectId === "string") runtime.projectId = body.projectId || undefined;
|
||||
if (typeof body.profileSchema === "string")
|
||||
runtime.profileSchema = body.profileSchema || undefined;
|
||||
if (body.planVersion === "lite" || body.planVersion === "pro")
|
||||
runtime.planVersion = body.planVersion;
|
||||
if (typeof body.topK === "number") runtime.topK = body.topK;
|
||||
if (typeof body.minScore === "number") runtime.minScore = body.minScore;
|
||||
if (typeof body.autoInject === "boolean") runtime.autoInject = body.autoInject;
|
||||
if (typeof body.injectEveryTurn === "boolean")
|
||||
runtime.injectEveryTurn = body.injectEveryTurn;
|
||||
if (typeof body.autoPersist === "boolean") runtime.autoPersist = body.autoPersist;
|
||||
|
||||
sendJson(res, 200, {
|
||||
ok: true,
|
||||
configured: runtime.apiKey !== undefined && runtime.apiKey.length > 0,
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
const client = new MemoryClient(
|
||||
apiKey,
|
||||
resolveBaseUrl(ctx, config.baseUrl),
|
||||
config,
|
||||
resolveUserId(ctx, config),
|
||||
);
|
||||
registerTools(ctx, client);
|
||||
registerLifecycle(ctx, client, config);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Pure credential classification and pairing shared by the plugins that call
|
||||
* pay-as-you-go DashScope APIs directly (memory, knowledge base) or through
|
||||
* `bl managed-agent` (agentstudio). No runtime imports — this module is safe
|
||||
* to load from tests and its rules are locked by `tests/credentials.test.ts`.
|
||||
*
|
||||
* TokenPlan keys (`sk-sp-`) and pay-as-you-go keys (`sk-ws-`) are not
|
||||
* interchangeable: the TokenPlan gateway 401s a pay-as-you-go key, and the
|
||||
* service APIs this package calls 401 or 404 a TokenPlan key. The LLM
|
||||
* provider row keeps its TokenPlan key under a dedicated env name
|
||||
* (`BAILIAN_TOKENPLAN_API_KEY`); every other plugin needs a pay-as-you-go key
|
||||
* and rejects a TokenPlan one up front instead of failing at request time.
|
||||
*
|
||||
* @module bailian-cli-dsh/shared/credentials
|
||||
*/
|
||||
|
||||
/**
|
||||
* Standard DashScope model-domain endpoint. It serves the model APIs plus the
|
||||
* memory v2 and knowledge indices the plugins call directly — but NOT
|
||||
* `/api/v1/agentstudio`, which lives on the workspace-scoped host.
|
||||
*/
|
||||
export const DASHSCOPE_DEFAULT_BASE_URL = "https://dashscope.aliyuncs.com";
|
||||
|
||||
/** Key prefix that marks a TokenPlan key (which service APIs reject). */
|
||||
export const TOKEN_PLAN_KEY_PREFIX = "sk-sp-";
|
||||
|
||||
/** Whether a key is shaped like a TokenPlan key (which service APIs reject). */
|
||||
export function isTokenPlanKey(apiKey: string): boolean {
|
||||
return apiKey.startsWith(TOKEN_PLAN_KEY_PREFIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a base URL points at the TokenPlan gateway. That gateway serves the
|
||||
* model-inference routes only — none of the service APIs this package calls,
|
||||
* including `/api/v1/agentstudio`, so requests to it 404.
|
||||
*/
|
||||
export function isTokenPlanEndpoint(baseUrl: string): boolean {
|
||||
try {
|
||||
return new URL(baseUrl).hostname.startsWith("token-plan.");
|
||||
} catch {
|
||||
// An unparseable URL fails the request later with its own diagnostics;
|
||||
// this check only classifies well-formed endpoints.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The standard error wording every plugin uses when it resolves a TokenPlan
|
||||
* key, so all three surfaces fail with one recognizable, actionable message.
|
||||
*/
|
||||
export function tokenPlanKeyRejection(plugin: string, capability: string): string {
|
||||
return (
|
||||
`${plugin}: the resolved API key is a TokenPlan key (${TOKEN_PLAN_KEY_PREFIX}…), which ` +
|
||||
`${capability} rejects. Use a pay-as-you-go key (sk-ws-): set \`apiKey\` in this row's ` +
|
||||
"config or $DASHSCOPE_API_KEY. TokenPlan keys belong on $BAILIAN_TOKENPLAN_API_KEY, " +
|
||||
"which only the `bailian-tokenplan` LLM provider reads."
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the `--api-key` / `--base-url` flags handed to `bl managed-agent run`.
|
||||
* Each resolved half ships independently:
|
||||
*
|
||||
* - A resolved key becomes `--api-key`, overriding bl's auth chain so an
|
||||
* active TokenPlan profile cannot substitute its own key.
|
||||
* - A resolved endpoint becomes `--base-url`, overriding the ACTIVE PROFILE's
|
||||
* base_url — the half that fixes the classic `Bailian API 404`, where a
|
||||
* TokenPlan (or bare model-domain) origin does not serve
|
||||
* `/api/v1/agentstudio`.
|
||||
*
|
||||
* There is deliberately NO fallback endpoint: agentstudio is only served on
|
||||
* the workspace-scoped host (see {@link workspaceEndpoint}), and an unknown
|
||||
* workspace is a configuration gap, not a defaultable value. Unresolved halves
|
||||
* emit nothing and bl's own auth chain decides them.
|
||||
*/
|
||||
export function credentialFlags(apiKey: string | undefined, baseUrl: string | undefined): string[] {
|
||||
const flags: string[] = [];
|
||||
if (baseUrl !== undefined && baseUrl.length > 0) flags.push("--base-url", baseUrl);
|
||||
if (apiKey !== undefined && apiKey.length > 0) flags.push("--api-key", apiKey);
|
||||
return flags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose the workspace-scoped agentstudio host for a workspace id. The
|
||||
* managed-agent API is served only from
|
||||
* `https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio`
|
||||
* (bl/the SDK append the resource path onto this origin); the plain
|
||||
* dashscope origin 404s it, and a key only unlocks its own workspace's host
|
||||
* (a mismatched one 403s `Endpoint.AccessDenied`).
|
||||
*/
|
||||
export function workspaceEndpoint(workspaceId: string): string {
|
||||
return `https://${workspaceId}.cn-beijing.maas.aliyuncs.com`;
|
||||
}
|
||||
@@ -6,8 +6,9 @@
|
||||
*/
|
||||
import type { Context } from "@deepseek-ai/cordis";
|
||||
import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
|
||||
import { DASHSCOPE_DEFAULT_BASE_URL } from "./credentials.ts";
|
||||
|
||||
export const DASHSCOPE_DEFAULT_BASE_URL = "https://dashscope.aliyuncs.com";
|
||||
export { DASHSCOPE_DEFAULT_BASE_URL } from "./credentials.ts";
|
||||
|
||||
/** A non-2xx DashScope response, carrying the server's own wording. */
|
||||
export class DashScopeError extends Error {
|
||||
@@ -22,8 +23,10 @@ export class DashScopeError extends Error {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the DashScope key from explicit config, then the launch environment
|
||||
* (process env, project `.env`, harness-home `.env`).
|
||||
* Resolve the DashScope key: explicit row config first, then the launch
|
||||
* environment (process env, project `.env`, harness-home `.env`). Callers
|
||||
* that get `undefined` decide their own failure mode — opt-in plugins reject
|
||||
* at boot, the managed-agent tool falls through to bl's own auth chain.
|
||||
*/
|
||||
export function resolveApiKey(ctx: Context, explicit?: string): string | undefined {
|
||||
if (explicit !== undefined && explicit.length > 0) return explicit;
|
||||
|
||||
@@ -0,0 +1,423 @@
|
||||
/**
|
||||
* `bailian-cli-dsh/tokenplan-usage` (Host half): provides two webServer
|
||||
* routes for the Client's "Bailian" settings page:
|
||||
*
|
||||
* 1. `POST /api/bailian/credentials` — saves AK/SK to the dedicated `dsh`
|
||||
* bl profile via `bl auth login --open-api --config dsh`. This generates
|
||||
* a fresh access_token and stores AK/SK + token in the profile. All
|
||||
* subsequent console calls read this profile.
|
||||
*
|
||||
* 2. `POST /api/bailian/tokenplan/usage` — fetches personal-edition
|
||||
* TokenPlan usage (3 console APIs) using the `dsh` profile credentials.
|
||||
* Takes only `{ region, site }`; AK/SK are already saved in the profile.
|
||||
*
|
||||
* Configuration UX: users save AK/SK once on the settings page. All future
|
||||
* Bailian plugins reuse the same `dsh` profile credentials.
|
||||
*
|
||||
* @module bailian-cli-dsh/tokenplan-usage
|
||||
*/
|
||||
import type { Context } from "@deepseek-ai/cordis";
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { runBl } from "../shared/bl.ts";
|
||||
import { defineTool } from "@deepseek-ai/dsh-tools";
|
||||
import type { JsonValue } from "@deepseek-ai/dsh-session";
|
||||
import { FEATURES, featureById, type FeatureParam } from "../features.ts";
|
||||
import z from "@deepseek-ai/schemastery";
|
||||
|
||||
/** Cordis plugin name used by loader diagnostics. */
|
||||
export const name = "bailian-tokenplan-usage";
|
||||
|
||||
/** Hard deps: bl via subprocess; routes need webServer; feature tools need tools. */
|
||||
export const inject = ["subprocess", "webServer", "tools"];
|
||||
|
||||
export interface Config {
|
||||
/** Alibaba Cloud Access Key ID. Fallback when not provided via UI. */
|
||||
accessKeyId?: string;
|
||||
/** Alibaba Cloud Access Key Secret. Fallback when not provided via UI. */
|
||||
accessKeySecret?: string;
|
||||
/** Console gateway region (default: cn-beijing). */
|
||||
consoleRegion?: string;
|
||||
/** Console site: domestic or international (default: domestic). */
|
||||
consoleSite?: "domestic" | "international";
|
||||
/** Dedicated bl config profile name (default: dsh). */
|
||||
profile?: string;
|
||||
}
|
||||
|
||||
export const Config = z.object({
|
||||
accessKeyId: z.string().description("Alibaba Cloud Access Key ID (fallback)."),
|
||||
accessKeySecret: z.string().description("Alibaba Cloud Access Key Secret (fallback)."),
|
||||
consoleRegion: z.string().description("Console gateway region (default: cn-beijing)."),
|
||||
consoleSite: z.string().description("Console site: domestic or international."),
|
||||
profile: z.string().description("Dedicated bl config profile name (default: dsh)."),
|
||||
});
|
||||
|
||||
/** Personal-edition console API names (from bailian-tokenplan frontend). */
|
||||
const PERSONAL_USAGE_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage";
|
||||
const PERSONAL_SUBSCRIPTION_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/subscription";
|
||||
const PERSONAL_ADDON_SUMMARY_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/addon/summary";
|
||||
|
||||
const PERSONAL_SUB_COMMODITY_CN = "sfm_tokenplansolo_public_cn";
|
||||
const PERSONAL_SUB_COMMODITY_INTL = "sfm_tokenplansolo_public_intl";
|
||||
const PERSONAL_ADDON_COMMODITY_CN = "sfm_tokenplansoloaddon_public_cn";
|
||||
const PERSONAL_ADDON_COMMODITY_INTL = "sfm_tokenplansoloaddon_public_intl";
|
||||
|
||||
const CREDENTIALS_ROUTE = "/bailian/credentials";
|
||||
const USAGE_ROUTE = "/bailian/tokenplan/usage";
|
||||
const CONSOLE_ROUTE = "/bailian/console";
|
||||
const BL_LOGIN_TIMEOUT_MS = 30_000;
|
||||
const BL_CALL_TIMEOUT_MS = 90_000;
|
||||
const BL_LOGIN_GRACE_MS = 20_000;
|
||||
const BL_CALL_GRACE_MS = 60_000;
|
||||
const DEFAULT_PROFILE = "dsh";
|
||||
|
||||
interface FetchResult {
|
||||
usage: unknown;
|
||||
subscription: unknown;
|
||||
addonSummary: unknown;
|
||||
errors: Array<{ api: string; message: string }>;
|
||||
}
|
||||
|
||||
/** Extract the business payload from a console gateway response. */
|
||||
function extractData(response: unknown): unknown {
|
||||
if (response === null || typeof response !== "object") return response;
|
||||
const outer = (response as Record<string, unknown>).data;
|
||||
if (outer !== null && typeof outer === "object") {
|
||||
const dataV2 = (outer as Record<string, unknown>).DataV2;
|
||||
if (dataV2 !== null && typeof dataV2 === "object") {
|
||||
const inner = (dataV2 as Record<string, unknown>).data;
|
||||
if (inner !== null && typeof inner === "object") {
|
||||
const payload = (inner as Record<string, unknown>).data;
|
||||
if (payload !== undefined) return payload;
|
||||
return inner;
|
||||
}
|
||||
}
|
||||
const fallback = (outer as Record<string, unknown>).data;
|
||||
if (fallback !== undefined) return fallback;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/** Read a UTF-8 POST body up to a size limit. */
|
||||
function readJsonBody(req: IncomingMessage, maxBytes: number = 8192): Promise<unknown> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
let total = 0;
|
||||
req.on("data", (chunk: Buffer) => {
|
||||
total += chunk.length;
|
||||
if (total > maxBytes) {
|
||||
req.destroy();
|
||||
reject(new Error("request body too large"));
|
||||
return;
|
||||
}
|
||||
chunks.push(chunk);
|
||||
});
|
||||
req.on("end", () => {
|
||||
const text = Buffer.concat(chunks).toString("utf8");
|
||||
if (text.length === 0) return resolve({});
|
||||
try {
|
||||
resolve(JSON.parse(text));
|
||||
} catch {
|
||||
reject(new Error("invalid JSON body"));
|
||||
}
|
||||
});
|
||||
req.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
/** Send a JSON response with a status code. */
|
||||
function sendJson(res: ServerResponse, status: number, data: unknown): void {
|
||||
res.statusCode = status;
|
||||
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
||||
res.end(JSON.stringify(data));
|
||||
}
|
||||
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
const webServer = ctx.get("webServer");
|
||||
if (webServer === undefined) return;
|
||||
|
||||
const profile = config.profile || DEFAULT_PROFILE;
|
||||
|
||||
/** Save AK/SK to the dsh profile (bl auth login --open-api --config dsh). */
|
||||
async function saveCredentials(accessKeyId: string, accessKeySecret: string): Promise<void> {
|
||||
const loginArgs = [
|
||||
"auth",
|
||||
"login",
|
||||
"--open-api",
|
||||
"--config",
|
||||
profile,
|
||||
"--access-key-id",
|
||||
accessKeyId,
|
||||
"--access-key-secret",
|
||||
accessKeySecret,
|
||||
];
|
||||
const loginOutcome = await runBl(ctx, loginArgs, {
|
||||
cwd: process.cwd(),
|
||||
signal: AbortSignal.timeout(BL_LOGIN_TIMEOUT_MS),
|
||||
graceMs: BL_LOGIN_GRACE_MS,
|
||||
});
|
||||
if (loginOutcome.exitCode !== 0) {
|
||||
const reason =
|
||||
loginOutcome.stderr.trim() || loginOutcome.stdout.trim() || `exit ${loginOutcome.exitCode}`;
|
||||
throw new Error(`bl auth login failed: ${reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Call a console API using the dsh profile (credentials already saved). */
|
||||
async function consoleCall(
|
||||
region: string,
|
||||
site: string,
|
||||
api: string,
|
||||
data: Record<string, unknown>,
|
||||
): Promise<unknown> {
|
||||
const callArgs = [
|
||||
"console",
|
||||
"call",
|
||||
"--config",
|
||||
profile,
|
||||
"--api",
|
||||
api,
|
||||
"--data",
|
||||
JSON.stringify(data),
|
||||
"--console-region",
|
||||
region,
|
||||
"--console-site",
|
||||
site,
|
||||
"--output",
|
||||
"json",
|
||||
];
|
||||
const callOutcome = await runBl(ctx, callArgs, {
|
||||
cwd: process.cwd(),
|
||||
signal: AbortSignal.timeout(BL_CALL_TIMEOUT_MS),
|
||||
graceMs: BL_CALL_GRACE_MS,
|
||||
});
|
||||
if (callOutcome.exitCode !== 0) {
|
||||
const reason =
|
||||
callOutcome.stderr.trim() || callOutcome.stdout.trim() || `exit ${callOutcome.exitCode}`;
|
||||
throw new Error(`bl console call failed (${api}): ${reason}`);
|
||||
}
|
||||
try {
|
||||
return JSON.parse(callOutcome.stdout);
|
||||
} catch {
|
||||
return { raw: callOutcome.stdout };
|
||||
}
|
||||
}
|
||||
|
||||
/** Fetch all personal-edition TokenPlan usage (3 console calls). */
|
||||
async function fetchUsage(region: string, site: string): Promise<FetchResult> {
|
||||
const isIntl = site === "international";
|
||||
const subCommodity = isIntl ? PERSONAL_SUB_COMMODITY_INTL : PERSONAL_SUB_COMMODITY_CN;
|
||||
const addonCommodity = isIntl ? PERSONAL_ADDON_COMMODITY_INTL : PERSONAL_ADDON_COMMODITY_CN;
|
||||
|
||||
const errors: Array<{ api: string; message: string }> = [];
|
||||
let usage = null;
|
||||
let subscription = null;
|
||||
let addonSummary = null;
|
||||
|
||||
try {
|
||||
usage = extractData(await consoleCall(region, site, PERSONAL_USAGE_API, {}));
|
||||
} catch (error) {
|
||||
errors.push({
|
||||
api: "usage",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
try {
|
||||
subscription = extractData(
|
||||
await consoleCall(region, site, PERSONAL_SUBSCRIPTION_API, {
|
||||
queryInstanceInfoRequest: { commodityCode: subCommodity },
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
errors.push({
|
||||
api: "subscription",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
try {
|
||||
addonSummary = extractData(
|
||||
await consoleCall(region, site, PERSONAL_ADDON_SUMMARY_API, {
|
||||
commodityCode: addonCommodity,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
errors.push({
|
||||
api: "addonSummary",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
|
||||
return { usage, subscription, addonSummary, errors };
|
||||
}
|
||||
|
||||
// Route 1: Save credentials to the dsh bl profile.
|
||||
ctx.effect(() =>
|
||||
webServer.register({
|
||||
kind: "exact",
|
||||
path: CREDENTIALS_ROUTE,
|
||||
handler: async (req: IncomingMessage, res: ServerResponse) => {
|
||||
if (req.method !== "POST") {
|
||||
sendJson(res, 405, { error: "method not allowed, use POST" });
|
||||
return;
|
||||
}
|
||||
let body: Record<string, unknown>;
|
||||
try {
|
||||
body = (await readJsonBody(req)) as Record<string, unknown>;
|
||||
} catch (error) {
|
||||
sendJson(res, 400, { error: error instanceof Error ? error.message : "bad request" });
|
||||
return;
|
||||
}
|
||||
const accessKeyId = (body.accessKeyId as string) || config.accessKeyId;
|
||||
const accessKeySecret = (body.accessKeySecret as string) || config.accessKeySecret;
|
||||
if (!accessKeyId || !accessKeySecret) {
|
||||
sendJson(res, 400, { error: "accessKeyId and accessKeySecret are required." });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await saveCredentials(accessKeyId, accessKeySecret);
|
||||
sendJson(res, 200, { ok: true, profile });
|
||||
} catch (error) {
|
||||
sendJson(res, 500, { error: error instanceof Error ? error.message : "internal error" });
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Route 2: Fetch TokenPlan usage using the dsh profile credentials.
|
||||
ctx.effect(() =>
|
||||
webServer.register({
|
||||
kind: "exact",
|
||||
path: USAGE_ROUTE,
|
||||
handler: async (req: IncomingMessage, res: ServerResponse) => {
|
||||
if (req.method !== "POST") {
|
||||
sendJson(res, 405, { error: "method not allowed, use POST" });
|
||||
return;
|
||||
}
|
||||
let body: Record<string, unknown>;
|
||||
try {
|
||||
body = (await readJsonBody(req)) as Record<string, unknown>;
|
||||
} catch (error) {
|
||||
sendJson(res, 400, { error: error instanceof Error ? error.message : "bad request" });
|
||||
return;
|
||||
}
|
||||
// If AK/SK are provided in the body, save them first (auto-provision).
|
||||
const bodyKeyId = (body.accessKeyId as string) || undefined;
|
||||
const bodyKeySecret = (body.accessKeySecret as string) || undefined;
|
||||
if (bodyKeyId && bodyKeySecret) {
|
||||
try {
|
||||
await saveCredentials(bodyKeyId, bodyKeySecret);
|
||||
} catch (error) {
|
||||
sendJson(res, 500, {
|
||||
error: error instanceof Error ? error.message : "credential save failed",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
const region = (body.region as string) || config.consoleRegion || "cn-beijing";
|
||||
const site = (body.site as string) || config.consoleSite || "domestic";
|
||||
try {
|
||||
const result = await fetchUsage(region, site);
|
||||
sendJson(res, 200, result);
|
||||
} catch (error) {
|
||||
sendJson(res, 500, { error: error instanceof Error ? error.message : "internal error" });
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// ── Feature layer: reuse bailian-cli commands as model tools + a generic route ──
|
||||
|
||||
/** Run a feature's `bl` command with the dsh profile; returns parsed JSON. */
|
||||
async function invokeFeature(
|
||||
feature: (typeof FEATURES)[number],
|
||||
params?: Record<string, unknown>,
|
||||
): Promise<JsonValue> {
|
||||
const extra: string[] = [];
|
||||
for (const pf of feature.paramFlags ?? []) {
|
||||
const val = params?.[pf.name];
|
||||
if (val !== undefined && val !== null && val !== "") extra.push(pf.flag, String(val));
|
||||
}
|
||||
if (extra.length === 0 && feature.defaultArgs) extra.push(...feature.defaultArgs);
|
||||
const args = [...feature.argv, ...extra, "--config", profile, "--output", "json"];
|
||||
const outcome = await runBl(ctx, args, {
|
||||
cwd: process.cwd(),
|
||||
signal: AbortSignal.timeout(BL_CALL_TIMEOUT_MS),
|
||||
graceMs: BL_CALL_GRACE_MS,
|
||||
});
|
||||
if (outcome.exitCode !== 0) {
|
||||
const reason = outcome.stderr.trim() || outcome.stdout.trim() || `exit ${outcome.exitCode}`;
|
||||
throw new Error(`bl ${feature.argv.join(" ")} failed: ${reason}`);
|
||||
}
|
||||
try {
|
||||
return JSON.parse(outcome.stdout);
|
||||
} catch {
|
||||
return { raw: outcome.stdout };
|
||||
}
|
||||
}
|
||||
|
||||
// Natural-language entry: one model tool per feature.
|
||||
const tools = ctx.get("tools");
|
||||
if (tools !== undefined) {
|
||||
for (const feature of FEATURES) {
|
||||
// Keep FeatureParam's literal `type` union: widening it to `string`
|
||||
// makes the map unassignable to ParameterSchemaSpec.
|
||||
const parameters: Record<string, { type: FeatureParam["type"]; description: string }> = {};
|
||||
for (const pf of feature.paramFlags ?? []) {
|
||||
parameters[pf.name] = { type: pf.type, description: pf.description };
|
||||
}
|
||||
ctx.effect(() =>
|
||||
tools.register(
|
||||
defineTool({
|
||||
name: `bailian_${feature.id}`,
|
||||
description: `${feature.title}。${feature.intent}`,
|
||||
parameters,
|
||||
output: {
|
||||
schema: { type: "object", additionalProperties: true },
|
||||
render: (_a, value) => [
|
||||
{ type: "text", text: String((value as any).summary ?? JSON.stringify(value)) },
|
||||
],
|
||||
},
|
||||
async execute(args) {
|
||||
const data = await invokeFeature(feature, args as Record<string, unknown>);
|
||||
return { summary: feature.summarize(data), data };
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Card-click entry: generic route dispatching to a feature by id.
|
||||
ctx.effect(() =>
|
||||
webServer.register({
|
||||
kind: "exact",
|
||||
path: CONSOLE_ROUTE,
|
||||
handler: async (req: IncomingMessage, res: ServerResponse) => {
|
||||
if (req.method !== "POST") {
|
||||
sendJson(res, 405, { error: "method not allowed, use POST" });
|
||||
return;
|
||||
}
|
||||
let body: Record<string, unknown>;
|
||||
try {
|
||||
body = (await readJsonBody(req)) as Record<string, unknown>;
|
||||
} catch (error) {
|
||||
sendJson(res, 400, { error: error instanceof Error ? error.message : "bad request" });
|
||||
return;
|
||||
}
|
||||
const feature = featureById(String(body.featureId ?? ""));
|
||||
if (feature === undefined) {
|
||||
sendJson(res, 400, { error: `unknown featureId: ${String(body.featureId)}` });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = await invokeFeature(
|
||||
feature,
|
||||
body.params as Record<string, unknown> | undefined,
|
||||
);
|
||||
sendJson(res, 200, { summary: feature.summarize(data), data });
|
||||
} catch (error) {
|
||||
sendJson(res, 500, { error: error instanceof Error ? error.message : "internal error" });
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
/**
|
||||
* `bailian-cli-dsh/tool-image`: image generation through `bl image generate`.
|
||||
*
|
||||
* Delegating to the CLI keeps the async-task polling, model-to-endpoint
|
||||
* routing, and artifact download in one place rather than restating them here.
|
||||
*
|
||||
* Generated files are committed to `ctx.attachments` and returned as
|
||||
* `ImageBlock`s when the calling route declares image input. When it does not
|
||||
* — DeepSeek routes never do — the tool degrades to reporting the saved paths
|
||||
* instead of failing, so the model can hand one to `bailian_vision_describe`.
|
||||
* For that fallback to work the files must survive the call, so this tool
|
||||
* deliberately does not delete what the CLI wrote.
|
||||
*
|
||||
* @module bailian-cli-dsh/tool-image
|
||||
*/
|
||||
import type { Context } from "@deepseek-ai/cordis";
|
||||
import type { ImageAttachmentRef, ImageMediaType } from "@deepseek-ai/dsh-attachment";
|
||||
import { AttachmentId } from "@deepseek-ai/dsh-attachment";
|
||||
import type { ContentBlock } from "@deepseek-ai/dsh-llm";
|
||||
import type {} from "@deepseek-ai/dsh-fs";
|
||||
import { defineTool } from "@deepseek-ai/dsh-tools";
|
||||
import type { ToolExecution } from "@deepseek-ai/dsh-tools";
|
||||
import z from "@deepseek-ai/schemastery";
|
||||
import { runBlJson } from "../shared/bl.ts";
|
||||
|
||||
/** Cordis plugin name used by loader diagnostics. */
|
||||
export const name = "bailian-tool-image";
|
||||
|
||||
/** Seams this plugin registers into. */
|
||||
export const inject = ["tools", "subprocess", "fs"];
|
||||
|
||||
export interface Config {
|
||||
/** Image model passed to `bl image generate --model`. */
|
||||
model?: string;
|
||||
/** Directory for generated files; defaults to the CLI's own output dir. */
|
||||
outDir?: string;
|
||||
/** Cooperative budget; async models poll until the task succeeds. */
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
export const Config: z<Config> = z.object({
|
||||
model: z.string().description("Image model; defaults to the CLI's own default."),
|
||||
outDir: z.string().description("Directory for generated files."),
|
||||
timeoutMs: z.natural().description("Cooperative timeout budget in milliseconds."),
|
||||
});
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 300_000;
|
||||
const MAX_IMAGES = 6;
|
||||
|
||||
const MEDIA_TYPE_BY_EXTENSION: Readonly<Record<string, ImageMediaType>> = {
|
||||
png: "image/png",
|
||||
jpg: "image/jpeg",
|
||||
jpeg: "image/jpeg",
|
||||
webp: "image/webp",
|
||||
gif: "image/gif",
|
||||
};
|
||||
|
||||
interface ImageGenerateResponse {
|
||||
urls?: readonly string[];
|
||||
saved?: readonly string[];
|
||||
total?: number;
|
||||
}
|
||||
|
||||
/** One committed image, stored as plain JSON so `render` stays pure. */
|
||||
interface CommittedImage {
|
||||
attachmentId: string;
|
||||
mediaType: ImageMediaType;
|
||||
bytes: number;
|
||||
width: number;
|
||||
height: number;
|
||||
path: string;
|
||||
}
|
||||
|
||||
function mediaTypeOf(path: string): ImageMediaType | undefined {
|
||||
const extension = path.split(".").pop()?.toLowerCase();
|
||||
return extension === undefined ? undefined : MEDIA_TYPE_BY_EXTENSION[extension];
|
||||
}
|
||||
|
||||
function attachmentRefOf(image: CommittedImage): ImageAttachmentRef {
|
||||
return {
|
||||
attachmentId: AttachmentId(image.attachmentId),
|
||||
mediaType: image.mediaType,
|
||||
bytes: image.bytes,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the calling route declares image input. Unlike `read_image`'s hard
|
||||
* gate this only reports, because an unroutable or text-only model is a reason
|
||||
* to fall back to paths rather than to refuse generating anything.
|
||||
*/
|
||||
async function routeAcceptsImages(ctx: Context, exec: ToolExecution): Promise<boolean> {
|
||||
const routed = exec.agent?.session.requestHeader()?.config;
|
||||
const provider = routed?.provider ?? exec.agent?.options.provider;
|
||||
const model = routed?.model ?? exec.agent?.options.model;
|
||||
const llm = ctx.get("llm");
|
||||
if (provider === undefined || model === undefined || llm === undefined) return false;
|
||||
try {
|
||||
const active = await llm.resolveModelInfo(provider, model, exec.signal);
|
||||
return active.inputModalities?.includes("image") === true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
ctx.tools.register(
|
||||
defineTool({
|
||||
name: "bailian_image_generate",
|
||||
description:
|
||||
"Generate images from a text prompt using Aliyun Bailian (Qwen-Image / Wan). " +
|
||||
"Files are written to disk and returned inline when the active model can view " +
|
||||
"images; otherwise the saved paths are reported and you can inspect one with " +
|
||||
"`bailian_vision_describe`.",
|
||||
parameters: {
|
||||
prompt: {
|
||||
type: "string",
|
||||
required: true,
|
||||
description: "What to depict. Be specific about subject, style, and composition.",
|
||||
},
|
||||
model: { type: "string", description: "Override the configured image model." },
|
||||
size: {
|
||||
type: "string",
|
||||
description: 'Aspect ratio such as "1:1" / "16:9", or explicit pixels as "1024*1024".',
|
||||
},
|
||||
n: {
|
||||
type: "integer",
|
||||
description: `How many images to generate (1-${MAX_IMAGES}).`,
|
||||
},
|
||||
negative_prompt: { type: "string", description: "What to avoid depicting." },
|
||||
seed: { type: "integer", description: "Seed for reproducible generation." },
|
||||
},
|
||||
output: {
|
||||
schema: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
images: {
|
||||
type: "array",
|
||||
required: true,
|
||||
items: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
attachmentId: { type: "string", required: true },
|
||||
mediaType: { type: "string", required: true },
|
||||
bytes: { type: "integer", required: true },
|
||||
width: { type: "integer", required: true },
|
||||
height: { type: "integer", required: true },
|
||||
path: { type: "string", required: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
paths: { type: "array", required: true, items: { type: "string" } },
|
||||
urls: { type: "array", required: true, items: { type: "string" } },
|
||||
},
|
||||
},
|
||||
render: (_args, value) => {
|
||||
const paths = value.paths.join("\n");
|
||||
if (value.images.length === 0) {
|
||||
return [
|
||||
{
|
||||
type: "text",
|
||||
text:
|
||||
`Generated ${value.paths.length} image(s); the active model cannot view ` +
|
||||
`images, so they are on disk only. Use bailian_vision_describe to inspect ` +
|
||||
`one.\n${paths}`,
|
||||
},
|
||||
];
|
||||
}
|
||||
const blocks: ContentBlock[] = [
|
||||
{ type: "text", text: `Generated ${value.images.length} image(s):\n${paths}` },
|
||||
];
|
||||
for (const image of value.images) {
|
||||
blocks.push({ type: "image", attachment: attachmentRefOf(image as CommittedImage) });
|
||||
}
|
||||
return blocks;
|
||||
},
|
||||
},
|
||||
timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
||||
async execute(args, exec) {
|
||||
if (args.n !== undefined && (args.n < 1 || args.n > MAX_IMAGES)) {
|
||||
throw new Error(`bailian_image_generate accepts n between 1 and ${MAX_IMAGES}.`);
|
||||
}
|
||||
|
||||
const cwd = exec.agent?.session.header.cwd ?? process.cwd();
|
||||
const argv = ["image", "generate", "--prompt", args.prompt];
|
||||
const model = args.model ?? config.model;
|
||||
if (model !== undefined) argv.push("--model", model);
|
||||
if (args.size !== undefined) argv.push("--size", args.size);
|
||||
if (args.n !== undefined) argv.push("--n", String(args.n));
|
||||
if (args.negative_prompt !== undefined)
|
||||
argv.push("--negative-prompt", args.negative_prompt);
|
||||
if (args.seed !== undefined) argv.push("--seed", String(args.seed));
|
||||
if (config.outDir !== undefined) argv.push("--out-dir", config.outDir);
|
||||
|
||||
const response = await runBlJson<ImageGenerateResponse>(ctx, argv, {
|
||||
cwd,
|
||||
signal: exec.signal,
|
||||
});
|
||||
|
||||
const paths = [...(response.saved ?? [])];
|
||||
const urls = [...(response.urls ?? [])];
|
||||
if (paths.length === 0) {
|
||||
throw new Error("bl image generate reported no saved files.");
|
||||
}
|
||||
|
||||
const attachments = ctx.get("attachments");
|
||||
const images: CommittedImage[] = [];
|
||||
if (attachments !== undefined && (await routeAcceptsImages(ctx, exec))) {
|
||||
const byteCap = Math.min(
|
||||
attachments.imageLimits.maxImageBytes,
|
||||
attachments.imageLimits.maxMessageImageBytes,
|
||||
);
|
||||
for (const path of paths) {
|
||||
const mediaType = mediaTypeOf(path);
|
||||
if (mediaType === undefined || !attachments.imageLimits.mediaTypes.includes(mediaType))
|
||||
continue;
|
||||
const target = await ctx.fs.resolve(path, { cwd, signal: exec.signal });
|
||||
const data = await ctx.fs.readBytes(target, exec.signal, byteCap);
|
||||
const ref = await attachments.saveImage({ data, mediaType, name: target.displayPath });
|
||||
images.push({
|
||||
attachmentId: ref.attachmentId,
|
||||
mediaType: ref.mediaType,
|
||||
bytes: ref.bytes,
|
||||
width: ref.width,
|
||||
height: ref.height,
|
||||
path,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { images, paths, urls };
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
/**
|
||||
* `bailian-cli-dsh/tool-managed-agent`: run a task on a Bailian-hosted managed
|
||||
* agent, provisioned on demand, through `bl managed-agent run`.
|
||||
*
|
||||
* A plain tool rather than a `SubagentProvider`: in dsh's `web` profile every
|
||||
* `tool-subagent` row is disabled in the host plane (delegation tools live in
|
||||
* agent presets), and a subagent provider fixes one agent identity in config —
|
||||
* neither fits "the model describes an intent and a remote agent is created for
|
||||
* it". As a tool the model calls it directly and fills `instructions` from the
|
||||
* user's intent, so the remote agent's role is defined per task.
|
||||
*
|
||||
* The CLI does ensure+run in one step: it materializes (idempotently) a cloud
|
||||
* agent + environment under the given `agent` name on first use and reuses them
|
||||
* after, so no `agents.yaml` or prior `apply` is required. First use provisions
|
||||
* cloud resources — it may incur cost and take longer to start.
|
||||
*
|
||||
* @module bailian-cli-dsh/tool-managed-agent
|
||||
*/
|
||||
import type { Context } from "@deepseek-ai/cordis";
|
||||
import { defineTool } from "@deepseek-ai/dsh-tools";
|
||||
import type {} from "@deepseek-ai/dsh-tools";
|
||||
import z from "@deepseek-ai/schemastery";
|
||||
import { runBlJson } from "../shared/bl.ts";
|
||||
|
||||
/** Cordis plugin name used by loader diagnostics. */
|
||||
export const name = "bailian-tool-managed-agent";
|
||||
|
||||
/** Seams this plugin registers into. */
|
||||
export const inject = ["tools", "subprocess"];
|
||||
|
||||
/** Default agent identity provisioned and reused across calls. */
|
||||
const DEFAULT_AGENT = "dsh-remote-runner";
|
||||
|
||||
export interface Config {
|
||||
/** Agent identity to create/reuse; distinct names get distinct remote agents. */
|
||||
agent?: string;
|
||||
/** Model for the remote agent. */
|
||||
model?: string;
|
||||
/** Cooperative budget; first-run provisioning of a cloud environment is slow. */
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
export const Config: z<Config> = z.object({
|
||||
agent: z.string().description("Remote agent identity to create/reuse."),
|
||||
model: z.string().description("Model for the remote agent."),
|
||||
timeoutMs: z.natural().description("Cooperative timeout budget in milliseconds."),
|
||||
});
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 600_000;
|
||||
|
||||
/** The `bl managed-agent run --output json` envelope: a session-event list. */
|
||||
interface SessionRunResponse {
|
||||
session_id?: string;
|
||||
agent?: string;
|
||||
events?: readonly { type?: string; content?: unknown; role?: string }[];
|
||||
}
|
||||
|
||||
/** Assistant-visible text of a finished remote session. */
|
||||
function assistantText(response: SessionRunResponse): string {
|
||||
return (response.events ?? [])
|
||||
.filter((event) => event.type === "message" && typeof event.content === "string")
|
||||
.map((event) => event.content as string)
|
||||
.join("\n")
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
ctx.tools.register(
|
||||
defineTool({
|
||||
name: "bailian_run_remote_task",
|
||||
description:
|
||||
"Run a task on a Bailian-hosted cloud agent. Use for long-running or isolated work you " +
|
||||
"want executed remotely rather than in this session. A remote agent is created on demand " +
|
||||
"(and reused) — describe the role it should play through `instructions`, and the concrete " +
|
||||
"task through `task`. Returns the remote agent's final answer.",
|
||||
parameters: {
|
||||
task: {
|
||||
type: "string",
|
||||
required: true,
|
||||
description: "The concrete task for the remote agent to carry out.",
|
||||
},
|
||||
instructions: {
|
||||
type: "string",
|
||||
description:
|
||||
"Role/system instructions defining what the remote agent is good at. " +
|
||||
"Defaults to a generic assistant.",
|
||||
},
|
||||
model: {
|
||||
type: "string",
|
||||
description: "Override the configured model for this task.",
|
||||
},
|
||||
},
|
||||
output: {
|
||||
schema: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
answer: { type: "string", required: true },
|
||||
sessionId: { type: "string", required: true },
|
||||
},
|
||||
},
|
||||
render: (_args, value) => [{ type: "text", text: value.answer }],
|
||||
},
|
||||
timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
||||
async execute(args, exec) {
|
||||
const argv = [
|
||||
"managed-agent",
|
||||
"run",
|
||||
"--prompt",
|
||||
args.task,
|
||||
"--agent",
|
||||
config.agent ?? DEFAULT_AGENT,
|
||||
];
|
||||
if (args.instructions !== undefined) argv.push("--instructions", args.instructions);
|
||||
const model = args.model ?? config.model;
|
||||
if (model !== undefined) argv.push("--model", model);
|
||||
|
||||
const response = await runBlJson<SessionRunResponse>(ctx, argv, {
|
||||
cwd: exec.agent?.session.header.cwd ?? process.cwd(),
|
||||
signal: exec.signal,
|
||||
});
|
||||
|
||||
const answer = assistantText(response);
|
||||
if (answer.length === 0) {
|
||||
throw new Error("the remote agent produced no assistant output.");
|
||||
}
|
||||
return { answer, sessionId: response.session_id ?? "" };
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
/**
|
||||
* `bailian-cli-dsh/tool-vision`: image and video understanding through
|
||||
* `bl vision describe` (Qwen-VL).
|
||||
*
|
||||
* The tool returns TEXT, never an `ImageBlock` — that is deliberate. dsh gates
|
||||
* image content on the active route's declared input modalities in two places
|
||||
* before a plugin ever sees it (the Web UI paste pre-check and `read_image`),
|
||||
* so a text-only main model such as DeepSeek cannot receive pictures at all.
|
||||
* Handing back a description instead gives those routes vision indirectly.
|
||||
* A genuinely multimodal route does not need this tool and should paste images
|
||||
* directly.
|
||||
*
|
||||
* @module bailian-cli-dsh/tool-vision
|
||||
*/
|
||||
import type { Context } from "@deepseek-ai/cordis";
|
||||
import { defineTool } from "@deepseek-ai/dsh-tools";
|
||||
import type {} from "@deepseek-ai/dsh-tools";
|
||||
import z from "@deepseek-ai/schemastery";
|
||||
import { runBlJson } from "../shared/bl.ts";
|
||||
|
||||
/** Cordis plugin name used by loader diagnostics. */
|
||||
export const name = "bailian-tool-vision";
|
||||
|
||||
/** Seams this plugin registers into. */
|
||||
export const inject = ["tools", "subprocess"];
|
||||
|
||||
export interface Config {
|
||||
/** Vision model passed to `bl vision describe --model`. */
|
||||
model?: string;
|
||||
/** Cooperative budget; video understanding uploads and is slow. */
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
export const Config: z<Config> = z.object({
|
||||
model: z.string().description("Vision model; defaults to the CLI's own default."),
|
||||
timeoutMs: z.natural().description("Cooperative timeout budget in milliseconds."),
|
||||
});
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 180_000;
|
||||
|
||||
/** The OpenAI-shaped body `bl vision describe --output json` passes through. */
|
||||
interface VisionResponse {
|
||||
model?: string;
|
||||
request_id?: string;
|
||||
choices?: readonly {
|
||||
message?: { content?: unknown };
|
||||
}[];
|
||||
}
|
||||
|
||||
/** Chat content is a string or an array of typed parts; keep only the text. */
|
||||
function readContent(content: unknown): string {
|
||||
if (typeof content === "string") return content;
|
||||
if (!Array.isArray(content)) return "";
|
||||
return content
|
||||
.map((part) =>
|
||||
typeof part === "object" && part !== null && "text" in part
|
||||
? String((part as { text: unknown }).text)
|
||||
: "",
|
||||
)
|
||||
.join("")
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
ctx.tools.register(
|
||||
defineTool({
|
||||
name: "bailian_vision_describe",
|
||||
description:
|
||||
"Understand an image or video using Aliyun Bailian's Qwen-VL models. " +
|
||||
"Accepts a local file path or a URL and returns a text description, so it works " +
|
||||
"even when the active model cannot take image input. Ask a specific question " +
|
||||
"through `prompt` (for example OCR, chart reading, or object identification) " +
|
||||
"instead of relying on the generic default.",
|
||||
parameters: {
|
||||
image: {
|
||||
type: "string",
|
||||
description: "Local image path or http(s)/oss URL. Provide this or `video`.",
|
||||
},
|
||||
video: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description:
|
||||
"Video file paths or URLs (mp4/mov/avi/mkv/webm). Local files are uploaded first.",
|
||||
},
|
||||
prompt: {
|
||||
type: "string",
|
||||
description: "Question about the content. Defaults to a plain description request.",
|
||||
},
|
||||
model: {
|
||||
type: "string",
|
||||
description: "Override the configured vision model.",
|
||||
},
|
||||
},
|
||||
output: {
|
||||
schema: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
description: { type: "string", required: true },
|
||||
model: { type: "string", required: true },
|
||||
},
|
||||
},
|
||||
render: (_args, value) => [{ type: "text", text: value.description }],
|
||||
},
|
||||
timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
||||
isConcurrencySafe: () => true,
|
||||
async execute(args, exec) {
|
||||
const videos = args.video ?? [];
|
||||
if (args.image === undefined && videos.length === 0) {
|
||||
throw new Error("bailian_vision_describe requires `image` or `video`.");
|
||||
}
|
||||
|
||||
const argv = ["vision", "describe"];
|
||||
if (args.image !== undefined) argv.push("--image", args.image);
|
||||
for (const video of videos) argv.push("--video", video);
|
||||
if (args.prompt !== undefined) argv.push("--prompt", args.prompt);
|
||||
|
||||
const model = args.model ?? config.model;
|
||||
if (model !== undefined) argv.push("--model", model);
|
||||
|
||||
const response = await runBlJson<VisionResponse>(ctx, argv, {
|
||||
cwd: exec.agent?.session.header.cwd ?? process.cwd(),
|
||||
signal: exec.signal,
|
||||
});
|
||||
|
||||
const description = readContent(response.choices?.[0]?.message?.content);
|
||||
if (description.length === 0) {
|
||||
throw new Error("Qwen-VL returned an empty description.");
|
||||
}
|
||||
|
||||
return { description, model: response.model ?? model ?? "" };
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
/**
|
||||
* `bailian-cli-dsh/web-search-rag`: registers a Bailian knowledge-base
|
||||
* `WebSearchProvider` with `ctx.web`.
|
||||
*
|
||||
* Retrieval is modelled as a search provider rather than a bespoke tool so the
|
||||
* model reaches private corpora through the `web_search` it already knows —
|
||||
* no new tool, no new prompting. Calls go straight to DashScope because the
|
||||
* seam needs per-call control the CLI does not surface.
|
||||
*
|
||||
* One instance serves one knowledge base: `WebSearchRequest` carries only
|
||||
* `query` and `maxResults`, so the agent id has to come from config. Insert
|
||||
* additional rows with distinct ids to expose more than one.
|
||||
*
|
||||
* @module bailian-cli-dsh/web-search-rag
|
||||
*/
|
||||
import type { Context } from "@deepseek-ai/cordis";
|
||||
import type {
|
||||
WebSearchProvider,
|
||||
WebSearchRequest,
|
||||
WebSearchResult,
|
||||
WebSearchSource,
|
||||
} from "@deepseek-ai/dsh-web";
|
||||
import { WebError } from "@deepseek-ai/dsh-web";
|
||||
import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
|
||||
import z from "@deepseek-ai/schemastery";
|
||||
import { dashScopeFetch, resolveApiKey } from "../shared/http.ts";
|
||||
|
||||
/** Cordis plugin name used by loader diagnostics. */
|
||||
export const name = "bailian-web-search-rag";
|
||||
|
||||
/** The web seam this provider registers into. */
|
||||
export const inject = ["web"];
|
||||
|
||||
/** Stable provider id; pin it as `searchProvider` to disambiguate. */
|
||||
export const BAILIAN_KB_PROVIDER_ID = "bailian-kb";
|
||||
|
||||
export interface Config {
|
||||
/** DashScope key; falls back to `DASHSCOPE_API_KEY`. */
|
||||
apiKey?: string;
|
||||
/** Workspace id; also the retrieval host prefix. Falls back to `BAILIAN_WORKSPACE_ID`. */
|
||||
workspaceId?: string;
|
||||
/** Retrieval service id from the console's knowledge retrieval page. */
|
||||
agentId?: string;
|
||||
/** Default upper bound when the caller sets none. */
|
||||
maxResults?: number;
|
||||
}
|
||||
|
||||
export const Config: z<Config> = z.object({
|
||||
apiKey: z
|
||||
.string()
|
||||
.role("secret")
|
||||
.description("DashScope API key; defaults to $DASHSCOPE_API_KEY."),
|
||||
workspaceId: z.string().description("Bailian workspace id; defaults to $BAILIAN_WORKSPACE_ID."),
|
||||
agentId: z.string().description("Retrieval service (agent) id identifying the knowledge base."),
|
||||
maxResults: z.natural().description("Default source cap when the caller sets none."),
|
||||
});
|
||||
|
||||
const DEFAULT_MAX_RESULTS = 10;
|
||||
|
||||
interface KnowledgeSearchNode {
|
||||
score?: number;
|
||||
text?: string;
|
||||
metadata?: {
|
||||
title?: string;
|
||||
doc_id?: string;
|
||||
doc_name?: string;
|
||||
doc_url?: string;
|
||||
page_number?: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface KnowledgeSearchResponse {
|
||||
data?: { total?: number; nodes?: readonly KnowledgeSearchNode[] };
|
||||
}
|
||||
|
||||
export interface BailianKbProviderOptions {
|
||||
apiKey: string;
|
||||
workspaceId: string;
|
||||
agentId: string;
|
||||
maxResults: number;
|
||||
}
|
||||
|
||||
function isAbort(error: unknown): boolean {
|
||||
return error instanceof DOMException && error.name === "AbortError";
|
||||
}
|
||||
|
||||
/** The seam requires a URL; documents without one still deserve a stable identity. */
|
||||
function sourceUrl(node: KnowledgeSearchNode, index: number): string {
|
||||
const url = node.metadata?.doc_url;
|
||||
if (url !== undefined && url.length > 0) return url;
|
||||
return `bailian-kb://${node.metadata?.doc_id ?? `node-${index}`}`;
|
||||
}
|
||||
|
||||
export class BailianKbSearchProvider implements WebSearchProvider {
|
||||
readonly id = BAILIAN_KB_PROVIDER_ID;
|
||||
|
||||
constructor(private readonly options: BailianKbProviderOptions) {}
|
||||
|
||||
available(): boolean {
|
||||
return (
|
||||
this.options.apiKey.length > 0 &&
|
||||
this.options.workspaceId.length > 0 &&
|
||||
this.options.agentId.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
async search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult> {
|
||||
const limit = request.maxResults ?? this.options.maxResults;
|
||||
let response: KnowledgeSearchResponse;
|
||||
try {
|
||||
response = await dashScopeFetch<KnowledgeSearchResponse>({
|
||||
url: `https://${this.options.workspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/indices/knowledge/search`,
|
||||
method: "POST",
|
||||
apiKey: this.options.apiKey,
|
||||
body: { query: request.query, agent_id: this.options.agentId },
|
||||
signal,
|
||||
});
|
||||
} catch (error) {
|
||||
if (isAbort(error))
|
||||
throw new WebError("knowledge base search aborted", "WEB_ABORTED", { cause: error });
|
||||
const reason = error instanceof Error ? error.message : String(error);
|
||||
throw new WebError(`knowledge base search failed: ${reason}`, "WEB_PROVIDER_ERROR", {
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
|
||||
const nodes = (response.data?.nodes ?? []).slice(0, limit);
|
||||
const sources: WebSearchSource[] = nodes.map((node, index) => {
|
||||
const metadata = node.metadata ?? {};
|
||||
const title = metadata.doc_name ?? metadata.title;
|
||||
const text = node.text ?? "";
|
||||
return {
|
||||
url: sourceUrl(node, index),
|
||||
...(title !== undefined ? { title } : {}),
|
||||
...(text.length > 0 ? { snippet: text } : {}),
|
||||
};
|
||||
});
|
||||
|
||||
const content = nodes
|
||||
.map((node) => node.text ?? "")
|
||||
.filter((text) => text.length > 0)
|
||||
.join("\n\n");
|
||||
|
||||
// Truncation is the seam's job; report what this provider returned.
|
||||
return { ...(content.length > 0 ? { content } : {}), sources, truncated: false };
|
||||
}
|
||||
}
|
||||
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
const workspaceId =
|
||||
config.workspaceId ?? launchEnvironmentOf(ctx).get("BAILIAN_WORKSPACE_ID")?.value ?? "";
|
||||
|
||||
ctx.web.registerSearchProvider(
|
||||
new BailianKbSearchProvider({
|
||||
apiKey: resolveApiKey(ctx, config.apiKey) ?? "",
|
||||
workspaceId,
|
||||
agentId: config.agentId ?? "",
|
||||
maxResults: config.maxResults ?? DEFAULT_MAX_RESULTS,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { expect, test } from "vite-plus/test";
|
||||
import {
|
||||
credentialFlags,
|
||||
DASHSCOPE_DEFAULT_BASE_URL,
|
||||
isTokenPlanEndpoint,
|
||||
isTokenPlanKey,
|
||||
workspaceEndpoint,
|
||||
} from "../src/shared/credentials.ts";
|
||||
|
||||
// 行为锁定:两类 Key(sk-sp- TokenPlan / sk-ws- 按量付费)不可混用。
|
||||
// TokenPlan 网关 401 按量付费 Key,TokenPlan 网关只提供模型推理,不提供
|
||||
// 服务 API。managed-agent 的凭证两半独立下发:解析出 key 就显式
|
||||
// --api-key(不让 bl 用活动 profile 的 key),解析出端点就显式 --base-url
|
||||
// (不让 bl 用活动 profile 的端点)。agentstudio 只在工作空间前缀主机上提供,
|
||||
// 因此绝不存在"默认端点"——工作空间未知就是配置缺口,该报错而不是猜。
|
||||
// 这些共享函数来自早期版本(vision / image / managed-agent 等工具),
|
||||
// 工具已移除但凭证分类逻辑保留作为参考。
|
||||
|
||||
test("isTokenPlanKey classifies by prefix", () => {
|
||||
expect(isTokenPlanKey("sk-sp-abc123")).toBe(true);
|
||||
expect(isTokenPlanKey("sk-ws-abc123")).toBe(false);
|
||||
expect(isTokenPlanKey("")).toBe(false);
|
||||
});
|
||||
|
||||
test("isTokenPlanEndpoint classifies the gateway host", () => {
|
||||
expect(isTokenPlanEndpoint("https://token-plan.cn-beijing.maas.aliyuncs.com")).toBe(true);
|
||||
expect(
|
||||
isTokenPlanEndpoint("https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1"),
|
||||
).toBe(true);
|
||||
expect(isTokenPlanEndpoint(DASHSCOPE_DEFAULT_BASE_URL)).toBe(false);
|
||||
expect(isTokenPlanEndpoint(workspaceEndpoint("llm-x"))).toBe(false);
|
||||
// 不可解析的 URL 交给后续请求自己报错,这里只做形状分类。
|
||||
expect(isTokenPlanEndpoint("not a url")).toBe(false);
|
||||
});
|
||||
|
||||
test("workspaceEndpoint composes the workspace-scoped agentstudio host", () => {
|
||||
expect(workspaceEndpoint("llm-kpgesh4vqzf5gzv9")).toBe(
|
||||
"https://llm-kpgesh4vqzf5gzv9.cn-beijing.maas.aliyuncs.com",
|
||||
);
|
||||
expect(workspaceEndpoint("ws_abc")).toBe("https://ws_abc.cn-beijing.maas.aliyuncs.com");
|
||||
});
|
||||
|
||||
test("credentialFlags: each resolved half ships independently, no defaults", () => {
|
||||
expect(credentialFlags(undefined, undefined)).toEqual([]);
|
||||
expect(credentialFlags("", "")).toEqual([]);
|
||||
// 只有 key:端点留给 bl 解析,绝不塞一个会 404 的默认主机。
|
||||
expect(credentialFlags("sk-ws-abc", undefined)).toEqual(["--api-key", "sk-ws-abc"]);
|
||||
// 只有端点:也下发,key 留给 bl 的 auth chain。
|
||||
expect(credentialFlags(undefined, "https://ws.example.com")).toEqual([
|
||||
"--base-url",
|
||||
"https://ws.example.com",
|
||||
]);
|
||||
expect(credentialFlags("sk-ws-abc", "https://ws.example.com")).toEqual([
|
||||
"--base-url",
|
||||
"https://ws.example.com",
|
||||
"--api-key",
|
||||
"sk-ws-abc",
|
||||
]);
|
||||
});
|
||||
@@ -2,16 +2,7 @@ import { defineConfig } from "vite-plus";
|
||||
|
||||
export default defineConfig({
|
||||
pack: {
|
||||
// One entry per `exports` subpath: each dsh plugin row imports its own
|
||||
// module specifier, so they cannot share a bundle.
|
||||
entry: [
|
||||
"src/index.ts",
|
||||
"src/tool-vision/index.ts",
|
||||
"src/tool-image/index.ts",
|
||||
"src/tool-managed-agent/index.ts",
|
||||
"src/web-search-rag/index.ts",
|
||||
"src/memory/index.ts",
|
||||
],
|
||||
entry: ["src/index.ts", "src/tokenplan-usage/index.ts", "src/memory/index.ts"],
|
||||
minify: true,
|
||||
dts: {
|
||||
tsgo: true,
|
||||
|
||||
@@ -65,6 +65,8 @@ Use this index for the skill-scoped quick index and global flags.
|
||||
| `bl token-plan assign-seats` | Batch assign Token Plan seats to members | [token-plan.md](token-plan.md) |
|
||||
| `bl token-plan create-key` | Create a Token Plan API key for a seat | [token-plan.md](token-plan.md) |
|
||||
| `bl token-plan list-seats` | List Token Plan subscription seat details | [token-plan.md](token-plan.md) |
|
||||
| `bl token-plan personal-key` | Get the personal-edition TokenPlan API key (masked) for the current account | [token-plan.md](token-plan.md) |
|
||||
| `bl token-plan personal-usage` | Query personal-edition TokenPlan usage (5h/1w percentage, subscription, addon credits) | [token-plan.md](token-plan.md) |
|
||||
| `bl update` | Update the CLI to the latest or a specified version | [update.md](update.md) |
|
||||
| `bl usage free` | Query free-tier quota for models (all models if --model is omitted) | [usage.md](usage.md) |
|
||||
| `bl usage freetier` | Enable or disable auto-stop for free-tier models. Enables by default; use --off to disable | [usage.md](usage.md) |
|
||||
@@ -93,7 +95,7 @@ Use this index for the skill-scoped quick index and global flags.
|
||||
| `search` | `web` | [search.md](search.md) |
|
||||
| `skill` | `add`, `init`, `list`, `remove`, `update` | [skill.md](skill.md) |
|
||||
| `text` | `chat` | [text.md](text.md) |
|
||||
| `token-plan` | `add-member`, `assign-seats`, `create-key`, `list-seats` | [token-plan.md](token-plan.md) |
|
||||
| `token-plan` | `add-member`, `assign-seats`, `create-key`, `list-seats`, `personal-key`, `personal-usage` | [token-plan.md](token-plan.md) |
|
||||
| `update` | `(root)` | [update.md](update.md) |
|
||||
| `usage` | `free`, `freetier`, `stats`, `summary` | [usage.md](usage.md) |
|
||||
| `workspace` | `init`, `list` | [workspace.md](workspace.md) |
|
||||
|
||||
@@ -7,12 +7,14 @@ Index: [index.md](index.md)
|
||||
|
||||
## Commands in this group
|
||||
|
||||
| Command | Description |
|
||||
| ---------------------------- | ----------------------------------------- |
|
||||
| `bl token-plan add-member` | Add a member to a Token Plan organization |
|
||||
| `bl token-plan assign-seats` | Batch assign Token Plan seats to members |
|
||||
| `bl token-plan create-key` | Create a Token Plan API key for a seat |
|
||||
| `bl token-plan list-seats` | List Token Plan subscription seat details |
|
||||
| Command | Description |
|
||||
| ------------------------------ | -------------------------------------------------------------------------------------- |
|
||||
| `bl token-plan add-member` | Add a member to a Token Plan organization |
|
||||
| `bl token-plan assign-seats` | Batch assign Token Plan seats to members |
|
||||
| `bl token-plan create-key` | Create a Token Plan API key for a seat |
|
||||
| `bl token-plan list-seats` | List Token Plan subscription seat details |
|
||||
| `bl token-plan personal-key` | Get the personal-edition TokenPlan API key (masked) for the current account |
|
||||
| `bl token-plan personal-usage` | Query personal-edition TokenPlan usage (5h/1w percentage, subscription, addon credits) |
|
||||
|
||||
## Command details
|
||||
|
||||
@@ -153,3 +155,49 @@ bl token-plan list-seats --page-size 20 --status NORMAL
|
||||
```bash
|
||||
bl token-plan list-seats --query-assigned true --seat-type standard
|
||||
```
|
||||
|
||||
### `bl token-plan personal-key`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | --------------------------------------------------------------------------- |
|
||||
| **Name** | `token-plan personal-key` |
|
||||
| **Description** | Get the personal-edition TokenPlan API key (masked) for the current account |
|
||||
| **Usage** | `bl token-plan personal-key [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------------------ | ------ | -------- | -------------------------------------------------------- |
|
||||
| `--console-region <region>` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) |
|
||||
| `--console-site <site>` | string | no | Console site: domestic, international |
|
||||
| `--console-switch-agent <uid>` | number | no | Switch agent UID for delegated access |
|
||||
| `--workspace-id <id>` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl token-plan personal-key
|
||||
```
|
||||
|
||||
### `bl token-plan personal-usage`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | -------------------------------------------------------------------------------------- |
|
||||
| **Name** | `token-plan personal-usage` |
|
||||
| **Description** | Query personal-edition TokenPlan usage (5h/1w percentage, subscription, addon credits) |
|
||||
| **Usage** | `bl token-plan personal-usage [flags]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------------------ | ------ | -------- | -------------------------------------------------------- |
|
||||
| `--console-region <region>` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) |
|
||||
| `--console-site <site>` | string | no | Console site: domestic, international |
|
||||
| `--console-switch-agent <uid>` | number | no | Switch agent UID for delegated access |
|
||||
| `--workspace-id <id>` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl token-plan personal-usage
|
||||
```
|
||||
|
||||
@@ -53,6 +53,7 @@ Index: [index.md](index.md)
|
||||
#### Notes
|
||||
|
||||
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
|
||||
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
|
||||
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
|
||||
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
|
||||
|
||||
@@ -87,6 +88,7 @@ bl managed-agent apply --provider bailian --yes
|
||||
#### Notes
|
||||
|
||||
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
|
||||
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
|
||||
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
|
||||
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
|
||||
|
||||
@@ -153,6 +155,7 @@ bl managed-agent init --provider all
|
||||
#### Notes
|
||||
|
||||
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
|
||||
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
|
||||
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
|
||||
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
|
||||
- --no-refresh and --dry-run plan offline from local config and state: no remote requests, no state writes, provider keys are not checked.
|
||||
@@ -194,6 +197,7 @@ bl managed-agent plan --no-refresh
|
||||
#### Notes
|
||||
|
||||
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
|
||||
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
|
||||
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
|
||||
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
|
||||
- Unlike `apply`, this creates/updates the cloud agent + environment on demand without --yes. The first run provisions cloud resources (may incur cost and take longer to start); later runs with the same --agent reuse them.
|
||||
@@ -233,6 +237,7 @@ bl managed-agent run --prompt "Audit this dependency tree" --instructions "You a
|
||||
#### Notes
|
||||
|
||||
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
|
||||
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
|
||||
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
|
||||
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
|
||||
|
||||
@@ -271,6 +276,7 @@ bl managed-agent session create --agent assistant --title 'debug run'
|
||||
#### Notes
|
||||
|
||||
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
|
||||
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
|
||||
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
|
||||
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
|
||||
|
||||
@@ -303,6 +309,7 @@ bl managed-agent session delete --session-id sess_abc123
|
||||
#### Notes
|
||||
|
||||
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
|
||||
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
|
||||
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
|
||||
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
|
||||
|
||||
@@ -337,6 +344,7 @@ bl managed-agent session events --session-id sess_abc123 --all
|
||||
#### Notes
|
||||
|
||||
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
|
||||
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
|
||||
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
|
||||
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
|
||||
|
||||
@@ -368,6 +376,7 @@ bl managed-agent session get --session-id sess_abc123
|
||||
#### Notes
|
||||
|
||||
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
|
||||
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
|
||||
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
|
||||
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
|
||||
|
||||
@@ -412,6 +421,7 @@ bl managed-agent session list --all
|
||||
#### Notes
|
||||
|
||||
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
|
||||
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
|
||||
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
|
||||
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
|
||||
- --output json emits one envelope: { session_id, provider, agent, events } — read session_id to chain `session send/get/events/delete`.
|
||||
@@ -449,6 +459,7 @@ bl managed-agent session run --agent assistant --prompt "summarize this repo"
|
||||
#### Notes
|
||||
|
||||
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
|
||||
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
|
||||
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
|
||||
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
|
||||
|
||||
@@ -479,6 +490,7 @@ bl managed-agent session send --session-id sess_abc123 --message "continue"
|
||||
#### Notes
|
||||
|
||||
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
|
||||
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
|
||||
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
|
||||
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
|
||||
- Providers without a skill listing API (e.g. ark) return an empty list.
|
||||
@@ -525,6 +537,7 @@ bl managed-agent skill-list --source custom --provider bailian
|
||||
#### Notes
|
||||
|
||||
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
|
||||
- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.
|
||||
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
|
||||
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user