@ai-zen/agents-sdk 1.0.0-alpha.11 → 1.0.0-alpha.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +80 -0
- package/dist/agent-plugins/AutoMigratePlugin.d.ts +7 -7
- package/dist/agent-plugins/AutoMigratePlugin.js +8 -8
- package/dist/agent-plugins/ContextGuardPlugin.d.ts +13 -25
- package/dist/agent-plugins/ContextGuardPlugin.js +13 -20
- package/dist/config/ConfigManager.d.ts +6 -0
- package/dist/config/ConfigManager.js +17 -9
- package/dist/config/constants.js +34 -12
- package/dist/config/migrate.d.ts +47 -0
- package/dist/config/migrate.js +89 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -0
- package/dist/runtime/HandoffService.d.ts +2 -2
- package/dist/runtime/HandoffService.js +3 -3
- package/dist/runtime/createAgent.js +4 -1
- package/dist/runtime/createModel.d.ts +6 -1
- package/dist/runtime/createModel.js +8 -2
- package/dist/scope/plugins/builtin/BatchEditTool.js +4 -1
- package/dist/scope/plugins/builtin/EditTool.js +6 -1
- package/dist/scope/plugins/builtin/ViewImageTool.d.ts +6 -4
- package/dist/scope/plugins/builtin/ViewImageTool.js +40 -56
- package/dist/scope/plugins/mcp/McpScopePlugin.js +1 -1
- package/dist/scope/plugins/mcp/mcpTools.d.ts +1 -1
- package/dist/scope/plugins/mcp/mcpTools.js +85 -26
- package/dist/shared/content-blocks.d.ts +108 -0
- package/dist/shared/content-blocks.js +256 -0
- package/dist/shared/errors.d.ts +4 -8
- package/dist/shared/errors.js +8 -14
- package/dist/shared/reasoning.d.ts +64 -0
- package/dist/shared/reasoning.js +151 -0
- package/dist/types/index.d.ts +79 -6
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { PermissionPolicy, AgentPermissions, Endpoint, Model, ImageModel, AgentDefinition, AppConfig, ToolEnv, McpIcon, McpAnnotations, McpToolDef, McpResourceDef, McpPromptDef, McpServerManifest, McpServerConfig, McpConnectionState, } from "./types/index.js";
|
|
1
|
+
export type { PermissionPolicy, AgentPermissions, Endpoint, EndpointCapabilities, Model, ImageModel, AgentDefinition, AppConfig, ToolEnv, McpIcon, McpAnnotations, McpToolDef, McpResourceDef, McpPromptDef, McpServerManifest, McpServerConfig, McpConnectionState, } from "./types/index.js";
|
|
2
2
|
export { PermissionEvaluator } from "./scope/PermissionEvaluator.js";
|
|
3
3
|
export type { CandidateSets } from "./scope/PermissionEvaluator.js";
|
|
4
4
|
export { createDisclosureParam } from "./scope/disclosure.js";
|
|
@@ -23,6 +23,9 @@ export { ConfigManager, DEFAULT_AGENT_ID, DEFAULT_AGENT_DEFINITION, DEFAULT_SUBA
|
|
|
23
23
|
export { FACTORY_SKILLS, HANDOFF_SKILL_ID, FALLBACK_HANDOFF_TEMPLATE, isCustomSkill, } from "./config/factory-skills.js";
|
|
24
24
|
export { EntityRepository } from "./shared/EntityRepository.js";
|
|
25
25
|
export { AgentRepository } from "./crud/AgentRepository.js";
|
|
26
|
+
export { fileToContentSection, resolveModelAndEndpoint, supportsFilesApi, mimeOf, toDataUrl, base64ByteLength, contentHash, clearUploadCache, MIME_BY_EXT, DEFAULT_MAX_INLINE_BYTES, } from "./shared/content-blocks.js";
|
|
27
|
+
export type { FileSource, FileToSectionInput, FileToSectionResult, ContentTransport, UploadFn, } from "./shared/content-blocks.js";
|
|
28
|
+
export { applyReasoningEffort, capabilityOf, isReasoningEffort, modelAllowedEfforts, modelEfforts, readReasoningEffort, resolveModelParams, resolveReasoningEffort, } from "./shared/reasoning.js";
|
|
26
29
|
export { createModel } from "./runtime/createModel.js";
|
|
27
30
|
export { SdkAgent } from "./runtime/SdkAgent.js";
|
|
28
31
|
export type { AgentPlugin, SendContext } from "./runtime/SdkAgent.js";
|
package/dist/index.js
CHANGED
|
@@ -22,6 +22,10 @@ export { FACTORY_SKILLS, HANDOFF_SKILL_ID, FALLBACK_HANDOFF_TEMPLATE, isCustomSk
|
|
|
22
22
|
// CRUD — 实体仓储(仅 Agent 定义等能力实体)
|
|
23
23
|
export { EntityRepository } from "./shared/EntityRepository.js";
|
|
24
24
|
export { AgentRepository } from "./crud/AgentRepository.js";
|
|
25
|
+
// 文件 → 内容块(统一入口)— 上层应用构造媒体消息时请走这里,不要自己判断上传还是内联
|
|
26
|
+
export { fileToContentSection, resolveModelAndEndpoint, supportsFilesApi, mimeOf, toDataUrl, base64ByteLength, contentHash, clearUploadCache, MIME_BY_EXT, DEFAULT_MAX_INLINE_BYTES, } from "./shared/content-blocks.js";
|
|
27
|
+
// 思考强度(reasoning effort)— 能力声明、可用档位与参数写入
|
|
28
|
+
export { applyReasoningEffort, capabilityOf, isReasoningEffort, modelAllowedEfforts, modelEfforts, readReasoningEffort, resolveModelParams, resolveReasoningEffort, } from "./shared/reasoning.js";
|
|
25
29
|
// 运行时
|
|
26
30
|
export { createModel } from "./runtime/createModel.js";
|
|
27
31
|
export { SdkAgent } from "./runtime/SdkAgent.js";
|
|
@@ -37,7 +37,7 @@ export interface HandoffContext {
|
|
|
37
37
|
/** 当前 prompt token 用量(自动迁移时由插件提供;手动触发可省略) */
|
|
38
38
|
promptTokens?: number;
|
|
39
39
|
/** 上下文阈值(自动迁移时由插件提供;手动触发可省略) */
|
|
40
|
-
|
|
40
|
+
handoffTokens?: number;
|
|
41
41
|
/** 本次是否会在交接后开新会话(`migrate()` 为 true,`handoff()` 为 false)。钩子据此区分两种语义 */
|
|
42
42
|
migrating: boolean;
|
|
43
43
|
/** 交接前消息总数 */
|
|
@@ -83,7 +83,7 @@ export interface HandoffParams {
|
|
|
83
83
|
/** 当前 prompt token 用量(可选,仅透传给钩子) */
|
|
84
84
|
promptTokens?: number;
|
|
85
85
|
/** 上下文阈值(可选,仅透传给钩子) */
|
|
86
|
-
|
|
86
|
+
handoffTokens?: number;
|
|
87
87
|
/** 会话 id(可选,用于交接文件名,便于回溯) */
|
|
88
88
|
conversationId?: string;
|
|
89
89
|
}
|
|
@@ -194,7 +194,7 @@ export class HandoffService {
|
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
async runHandoff(params, migrating) {
|
|
197
|
-
const { agent, promptTokens,
|
|
197
|
+
const { agent, promptTokens, handoffTokens, conversationId } = params;
|
|
198
198
|
const requestedMode = params.mode ?? this.defaultMode;
|
|
199
199
|
const handoffPath = join(this.handoffDirPath, HandoffService.createHandoffFileName(new Date(), conversationId));
|
|
200
200
|
const model = this.model ?? agent.model;
|
|
@@ -207,7 +207,7 @@ export class HandoffService {
|
|
|
207
207
|
handoffPath,
|
|
208
208
|
model,
|
|
209
209
|
promptTokens,
|
|
210
|
-
|
|
210
|
+
handoffTokens,
|
|
211
211
|
migrating,
|
|
212
212
|
messageCountBefore,
|
|
213
213
|
historyText: "",
|
|
@@ -241,7 +241,7 @@ export class HandoffService {
|
|
|
241
241
|
handoffDoc,
|
|
242
242
|
model,
|
|
243
243
|
promptTokens,
|
|
244
|
-
|
|
244
|
+
handoffTokens,
|
|
245
245
|
migrating,
|
|
246
246
|
messageCountBefore,
|
|
247
247
|
historyText,
|
|
@@ -18,7 +18,10 @@ export async function createAgent(scope, agentId) {
|
|
|
18
18
|
const modelId = definition.modelId ?? scope.config.defaultModel;
|
|
19
19
|
if (!modelId)
|
|
20
20
|
throw new Error("未指定模型且无默认模型");
|
|
21
|
-
const { client, model, modelConfig } = createModel(scope, modelId
|
|
21
|
+
const { client, model, modelConfig } = createModel(scope, modelId, {
|
|
22
|
+
// Agent 级思考档位覆盖(未声明时为 undefined,行为与以前一致)
|
|
23
|
+
reasoningEffort: definition.reasoningEffort,
|
|
24
|
+
});
|
|
22
25
|
const tools = scope.buildTools(definition, {
|
|
23
26
|
exclude: {
|
|
24
27
|
subagents: definition.function?.name
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import OpenAI from "openai";
|
|
2
2
|
import type { Scope } from "../scope/Scope.js";
|
|
3
|
+
import type { AgentDefinition } from "../types/index.js";
|
|
3
4
|
/**
|
|
4
5
|
* Agent 模型装配结果:openai SDK client + 模型名 + 模型参数。
|
|
5
6
|
* Agent 构造时直接消费此结构。
|
|
@@ -17,5 +18,9 @@ export interface AgentModel {
|
|
|
17
18
|
*
|
|
18
19
|
* 端点(baseUrl / apiKey)来自 config.endpoints,发送给 API 的模型名 = modelName || id。
|
|
19
20
|
* 任何 OpenAI 兼容端点(OpenAI / 智谱 BigModel / DeepSeek 等)均可通过 baseURL 接入。
|
|
21
|
+
*
|
|
22
|
+
* `overrides.reasoningEffort` 是 **Agent 级覆盖**(`AgentDefinition.reasoningEffort`):
|
|
23
|
+
* 同一个模型可以给不同 Agent 配不同思考强度,而模型自身的 `defaultParams` 不变。
|
|
24
|
+
* 写入方式按模型的 `reasoning` 能力声明决定(见 `shared/reasoning.ts`)。
|
|
20
25
|
*/
|
|
21
|
-
export declare function createModel(scope: Scope, modelId: string): AgentModel;
|
|
26
|
+
export declare function createModel(scope: Scope, modelId: string, overrides?: Pick<AgentDefinition, "reasoningEffort">): AgentModel;
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import OpenAI from "openai";
|
|
2
|
+
import { resolveModelParams } from "../shared/reasoning.js";
|
|
2
3
|
/**
|
|
3
4
|
* 模型工厂 — 根据 modelId 和 Scope 装配 openai SDK client。
|
|
4
5
|
*
|
|
5
6
|
* 端点(baseUrl / apiKey)来自 config.endpoints,发送给 API 的模型名 = modelName || id。
|
|
6
7
|
* 任何 OpenAI 兼容端点(OpenAI / 智谱 BigModel / DeepSeek 等)均可通过 baseURL 接入。
|
|
8
|
+
*
|
|
9
|
+
* `overrides.reasoningEffort` 是 **Agent 级覆盖**(`AgentDefinition.reasoningEffort`):
|
|
10
|
+
* 同一个模型可以给不同 Agent 配不同思考强度,而模型自身的 `defaultParams` 不变。
|
|
11
|
+
* 写入方式按模型的 `reasoning` 能力声明决定(见 `shared/reasoning.ts`)。
|
|
7
12
|
*/
|
|
8
|
-
export function createModel(scope, modelId) {
|
|
13
|
+
export function createModel(scope, modelId, overrides) {
|
|
9
14
|
const config = scope.config;
|
|
10
15
|
const modelConfig = config.models.find((m) => m.id === modelId);
|
|
11
16
|
if (!modelConfig)
|
|
@@ -25,6 +30,7 @@ export function createModel(scope, modelId) {
|
|
|
25
30
|
return {
|
|
26
31
|
client,
|
|
27
32
|
model: apiModelName,
|
|
28
|
-
|
|
33
|
+
// 档位覆盖在这里生效:未声明 reasoningEffort 时原样返回模型 defaultParams
|
|
34
|
+
modelConfig: resolveModelParams(modelConfig, overrides),
|
|
29
35
|
};
|
|
30
36
|
}
|
|
@@ -3,7 +3,10 @@ import { SdkCallbackTool } from "./SdkCallbackTool.js";
|
|
|
3
3
|
export class BatchEditTool extends SdkCallbackTool {
|
|
4
4
|
function = {
|
|
5
5
|
name: "batch_edit",
|
|
6
|
-
description:
|
|
6
|
+
description: [
|
|
7
|
+
"批量编辑文件文本,可以优先使用这个工具对文件进行编辑。",
|
|
8
|
+
"对同一个文件的多处修改应在一次调用中全部提交(replacements 数组),而不是并行多次调用 edit——并行编辑同一文件会互相覆盖。",
|
|
9
|
+
].join("\n"),
|
|
7
10
|
parameters: {
|
|
8
11
|
type: "object",
|
|
9
12
|
properties: {
|
|
@@ -15,7 +15,12 @@ function countOccurrences(content, text) {
|
|
|
15
15
|
export class EditTool extends SdkCallbackTool {
|
|
16
16
|
function = {
|
|
17
17
|
name: "edit",
|
|
18
|
-
description:
|
|
18
|
+
description: [
|
|
19
|
+
"编辑文件中的文本,支持精确替换或替换所有匹配项。",
|
|
20
|
+
"",
|
|
21
|
+
"并发限制(重要):同一个文件禁止并行编辑。不要在同一轮回复中对同一路径发起多个 edit 调用——这些调用会各自读取旧内容再写回,后写者覆盖先写者,导致修改丢失或文件被写坏。",
|
|
22
|
+
"如需对同一个文件施加多处修改,请改用 batch_edit,在一次调用里提交全部替换。只有编辑不同文件时才可以并行调用 edit。",
|
|
23
|
+
].join("\n"),
|
|
19
24
|
parameters: {
|
|
20
25
|
type: "object",
|
|
21
26
|
properties: {
|
|
@@ -7,11 +7,13 @@ import type { AgentDefinition, AppConfig, ToolEnv } from "../../../types/index.j
|
|
|
7
7
|
* buildTools 阶段按声明过滤——非视觉模型不会拿到该工具,故无需运行时重复校验。
|
|
8
8
|
*
|
|
9
9
|
* 输入 `path_or_url`:
|
|
10
|
-
* - http(s) URL → 直接以 `image_url`
|
|
11
|
-
* - 本地路径 →
|
|
10
|
+
* - http(s) URL → 直接以 `image_url` 内容块返回;
|
|
11
|
+
* - 本地路径 → 交给 `fileToContentSection()` 统一处理:
|
|
12
|
+
* 端点声明 `capabilities.filesApi`(出厂仅 deepseek)→ Files API 上传,`file` 块(file_id);
|
|
13
|
+
* 其余端点 → base64 data URL 内联(`image_url`),不再要求必须配置 deepseek。
|
|
12
14
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
+
* 端点按**当前模型**反查(`ctx.agent.model`),不硬编码端点 id——`file_id` 是端点维度
|
|
16
|
+
* 资源,传错端点会返回 not found。
|
|
15
17
|
*/
|
|
16
18
|
export declare class ViewImageTool extends SdkCallbackTool {
|
|
17
19
|
/** 仅视觉模型可用:从 definition.modelId 解析当前模型,非视觉模型由 buildTools 过滤 */
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import OpenAI, { toFile } from "openai";
|
|
2
|
-
import { promises as fs } from "node:fs";
|
|
3
1
|
import path from "node:path";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
4
3
|
import { SdkCallbackTool } from "./SdkCallbackTool.js";
|
|
5
|
-
|
|
4
|
+
import { fileToContentSection, mimeOf, resolveModelAndEndpoint } from "../../../shared/content-blocks.js";
|
|
6
5
|
/**
|
|
7
6
|
* 是否为"本地 URL"——指向本地/回环地址,API 服务商无法下载访问。
|
|
8
7
|
* 覆盖 file:// 等本地协议,以及指向本机的 http(s) 地址(localhost、127.0.0.1、[::1]、0.0.0.0)。
|
|
@@ -19,11 +18,13 @@ function isLocalUrl(value) {
|
|
|
19
18
|
* buildTools 阶段按声明过滤——非视觉模型不会拿到该工具,故无需运行时重复校验。
|
|
20
19
|
*
|
|
21
20
|
* 输入 `path_or_url`:
|
|
22
|
-
* - http(s) URL → 直接以 `image_url`
|
|
23
|
-
* - 本地路径 →
|
|
21
|
+
* - http(s) URL → 直接以 `image_url` 内容块返回;
|
|
22
|
+
* - 本地路径 → 交给 `fileToContentSection()` 统一处理:
|
|
23
|
+
* 端点声明 `capabilities.filesApi`(出厂仅 deepseek)→ Files API 上传,`file` 块(file_id);
|
|
24
|
+
* 其余端点 → base64 data URL 内联(`image_url`),不再要求必须配置 deepseek。
|
|
24
25
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
26
|
+
* 端点按**当前模型**反查(`ctx.agent.model`),不硬编码端点 id——`file_id` 是端点维度
|
|
27
|
+
* 资源,传错端点会返回 not found。
|
|
27
28
|
*/
|
|
28
29
|
export class ViewImageTool extends SdkCallbackTool {
|
|
29
30
|
/** 仅视觉模型可用:从 definition.modelId 解析当前模型,非视觉模型由 buildTools 过滤 */
|
|
@@ -34,7 +35,7 @@ export class ViewImageTool extends SdkCallbackTool {
|
|
|
34
35
|
}
|
|
35
36
|
function = {
|
|
36
37
|
name: "view_image",
|
|
37
|
-
description: "查看一张图片并分析其内容。传入本地图片路径或 http(s) 图片 URL
|
|
38
|
+
description: "查看一张图片并分析其内容。传入本地图片路径或 http(s) 图片 URL。本地图片优先上传(端点支持 Files API 时)否则以 base64 内联引用,网络图片直接引用。不支持本地 URL(如 file:// 协议、指向本机 localhost/127.0.0.1 的 http(s) 地址),本地图片请使用文件路径。适合识别图片文字、描述图片内容、分析图表等。",
|
|
38
39
|
parameters: {
|
|
39
40
|
type: "object",
|
|
40
41
|
properties: {
|
|
@@ -57,60 +58,43 @@ export class ViewImageTool extends SdkCallbackTool {
|
|
|
57
58
|
return "view_image 需要提供 path_or_url 参数(本地图片路径或 http(s) URL)。";
|
|
58
59
|
}
|
|
59
60
|
const value = target.trim();
|
|
60
|
-
// 拒绝服务商无法访问的"本地 URL":file://
|
|
61
|
+
// 拒绝服务商无法访问的"本地 URL":file:// 等本地协议,或指向本机回环地址的 http(s) 地址
|
|
61
62
|
if (isLocalUrl(value)) {
|
|
62
63
|
return "不支持本地 URL(API 服务商无法下载访问)。本地图片请使用文件路径,而不是本地链接。";
|
|
63
64
|
}
|
|
65
|
+
// 按当前模型定位端点:决定走 Files API 上传还是 base64 内联
|
|
66
|
+
const { model, endpoint } = resolveModelAndEndpoint(this.env.config, ctx?.agent?.model);
|
|
64
67
|
try {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
stat = await fs.stat(absPath);
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
return `本地图片不存在: ${absPath}`;
|
|
85
|
-
}
|
|
86
|
-
if (!stat.isFile()) {
|
|
87
|
-
return `"${absPath}" 不是文件。`;
|
|
68
|
+
const isUrl = /^https?:\/\//i.test(value);
|
|
69
|
+
// 本工具只处理图片:先按扩展名把关,给出比通用入口更贴切的提示
|
|
70
|
+
if (!isUrl) {
|
|
71
|
+
const abs = this.resolve(value);
|
|
72
|
+
// 先 stat 一次,把 ENOENT 变成对人友好的提示(通用入口的报错面向任意文件)
|
|
73
|
+
try {
|
|
74
|
+
await fs.stat(abs);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return `本地图片不存在: ${abs}`;
|
|
78
|
+
}
|
|
79
|
+
const mime = mimeOf(abs);
|
|
80
|
+
if (!mime || !mime.startsWith("image/")) {
|
|
81
|
+
return `不支持的图片格式 "${path.extname(abs) || "(无扩展名)"}"。支持的格式:JPEG、PNG、GIF、WebP。`;
|
|
82
|
+
}
|
|
88
83
|
}
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const endpoint = (this.env.config.endpoints ?? []).find((e) => e.id === "deepseek");
|
|
95
|
-
if (!endpoint) {
|
|
96
|
-
return "未配置 deepseek 端点,无法上传本地图片(Files API)。如需查看本地图片,请先配置 deepseek 端点;网络图片 URL 可直接使用。";
|
|
97
|
-
}
|
|
98
|
-
if (!endpoint.apiKey) {
|
|
99
|
-
return `端点 "${endpoint.name}" 的 API Key 未设置,无法上传本地图片。`;
|
|
100
|
-
}
|
|
101
|
-
const client = new OpenAI({
|
|
102
|
-
apiKey: endpoint.apiKey,
|
|
103
|
-
baseURL: endpoint.baseUrl,
|
|
84
|
+
const result = await fileToContentSection({
|
|
85
|
+
source: isUrl ? { url: value } : { path: this.resolve(value) },
|
|
86
|
+
endpoint,
|
|
87
|
+
model,
|
|
88
|
+
signal,
|
|
104
89
|
});
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
];
|
|
90
|
+
const label = isUrl ? `已获取网络图片:${value}` : `已获取本地图片(${path.basename(value)})`;
|
|
91
|
+
const how = result.transport === "files-api"
|
|
92
|
+
? `,已上传至 Files API(file_id: ${result.section.file_id ?? "?"})`
|
|
93
|
+
: result.transport === "inline"
|
|
94
|
+
? `,已按 base64 内联(${(result.bytes / 1024).toFixed(0)} KB)`
|
|
95
|
+
: "";
|
|
96
|
+
const extra = result.warning ? `(降级说明:${result.warning})` : "";
|
|
97
|
+
return [{ type: "text", text: `${label}${how}${extra}` }, result.section];
|
|
114
98
|
}
|
|
115
99
|
catch (error) {
|
|
116
100
|
// 中断(abort)时返回明确的中断结果
|
|
@@ -51,7 +51,7 @@ export class McpScopePlugin {
|
|
|
51
51
|
result.push(createCallMcpTool(m, scope.env));
|
|
52
52
|
}
|
|
53
53
|
if (allow.has("read_mcp_resource") && m) {
|
|
54
|
-
result.push(createReadMcpResourceTool(m));
|
|
54
|
+
result.push(createReadMcpResourceTool(m, scope.env));
|
|
55
55
|
}
|
|
56
56
|
return result;
|
|
57
57
|
}
|
|
@@ -21,4 +21,4 @@ export declare function createCallMcpTool(mcpManager: McpConnectionManager, env:
|
|
|
21
21
|
/**
|
|
22
22
|
* 创建 read_mcp_resource 工具。
|
|
23
23
|
*/
|
|
24
|
-
export declare function createReadMcpResourceTool(mcpManager: McpConnectionManager): CallbackTool;
|
|
24
|
+
export declare function createReadMcpResourceTool(mcpManager: McpConnectionManager, env: ToolEnv): CallbackTool;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CallbackTool } from "@ai-zen/agents-core";
|
|
2
|
+
import { fileToContentSection, resolveModelAndEndpoint } from "../../../shared/content-blocks.js";
|
|
2
3
|
import { createDisclosureParam } from "../../disclosure.js";
|
|
3
4
|
import { DEFAULT_MAX_TOOL_OUTPUT, guardOutput, headTailPreview } from "../builtin/outputGuard.js";
|
|
4
5
|
import { getLogger } from "../../../shared/logger.js";
|
|
@@ -82,31 +83,75 @@ export function createLoadMcpTool(mcpManager, filteredMcps) {
|
|
|
82
83
|
},
|
|
83
84
|
});
|
|
84
85
|
}
|
|
85
|
-
/**
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
/** MIME → 文件名后缀(仅用于上传时给文件一个好名字) */
|
|
87
|
+
const EXT_BY_MIME = {
|
|
88
|
+
"image/png": "png",
|
|
89
|
+
"image/jpeg": "jpg",
|
|
90
|
+
"image/gif": "gif",
|
|
91
|
+
"image/webp": "webp",
|
|
92
|
+
"audio/wav": "wav",
|
|
93
|
+
"audio/mpeg": "mp3",
|
|
94
|
+
"application/pdf": "pdf",
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* MCP 媒体(base64 字符串)→ SDK 内容块。
|
|
98
|
+
*
|
|
99
|
+
* 统一交给 `fileToContentSection()` 决定传输方式:端点声明 Files API 能力就上传
|
|
100
|
+
* (`file` 块 + `file_id`,请求体小、不随历史反复膨胀),否则保持 base64 内联。
|
|
101
|
+
* 转换失败**不丢块**——退化为一条文本说明,避免整个工具调用因媒体问题失败。
|
|
102
|
+
*/
|
|
103
|
+
async function mediaSection(data, mime, media) {
|
|
104
|
+
const ext = EXT_BY_MIME[mime] ?? "bin";
|
|
105
|
+
try {
|
|
106
|
+
const { section } = await fileToContentSection({
|
|
107
|
+
source: {
|
|
108
|
+
base64: data,
|
|
109
|
+
mime,
|
|
110
|
+
filename: `mcp-media-${Date.now()}.${ext}`,
|
|
111
|
+
},
|
|
112
|
+
endpoint: media.endpoint,
|
|
113
|
+
model: media.model,
|
|
114
|
+
signal: media.signal,
|
|
115
|
+
});
|
|
116
|
+
return section;
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
return {
|
|
120
|
+
type: "text",
|
|
121
|
+
text: `[媒体] 无法转换(${mime}):${error?.message ?? error}`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* `read_mcp_resource` 的 resource contents → SDK 内容块。
|
|
127
|
+
*
|
|
128
|
+
* 注意形状差异:`callTool` 的 content 块带 `type` 判别字段,而 `readResource` 的
|
|
129
|
+
* contents 是 `{ uri, mimeType?, text }` 或 `{ uri, mimeType?, blob }`——**没有 type**。
|
|
130
|
+
* 若直接复用 `toMessageSection`,文本资源会掉进 default 分支被 JSON 化(实测踩过)。
|
|
131
|
+
*/
|
|
132
|
+
async function resourceToMessageSection(item, media) {
|
|
133
|
+
if (typeof item?.text === "string")
|
|
134
|
+
return { type: "text", text: item.text };
|
|
135
|
+
if (typeof item?.blob === "string") {
|
|
136
|
+
return mediaSection(item.blob, item.mimeType ?? "application/octet-stream", media);
|
|
137
|
+
}
|
|
138
|
+
return { type: "text", text: JSON.stringify(item) };
|
|
88
139
|
}
|
|
89
140
|
/**
|
|
90
141
|
* MCP content 块 → SDK 内容块。
|
|
91
142
|
*
|
|
92
143
|
* 文本类(text / resource.text / resource_link / 未知类型)归一为 `text` 块,
|
|
93
144
|
* 由调用方拼接成正文后进入输出保护;
|
|
94
|
-
* 媒体类(image / audio / resource.blob
|
|
95
|
-
*
|
|
145
|
+
* 媒体类(image / audio / resource.blob)经 `mediaSection()` → `fileToContentSection()`:
|
|
146
|
+
* 端点支持 Files API 时上传为 `file` 块,否则 base64 内联——图片不进字符闸门,也不丢弃。
|
|
96
147
|
*/
|
|
97
|
-
function toMessageSection(block) {
|
|
148
|
+
async function toMessageSection(block, media) {
|
|
98
149
|
switch (block?.type) {
|
|
99
150
|
case "image":
|
|
100
|
-
return
|
|
101
|
-
type: "image_url",
|
|
102
|
-
image_url: { url: toDataUrl(block.data, block.mimeType ?? "image/png") },
|
|
103
|
-
};
|
|
151
|
+
return await mediaSection(block.data, block.mimeType ?? "image/png", media);
|
|
104
152
|
case "audio":
|
|
105
|
-
// AgentNS 无独立音频块,以
|
|
106
|
-
return
|
|
107
|
-
type: "file",
|
|
108
|
-
file_data: toDataUrl(block.data, block.mimeType ?? "audio/wav"),
|
|
109
|
-
};
|
|
153
|
+
// AgentNS 无独立音频块,以 `file` 块承载(支持上传的端点 → file_id,否则 file_data 内联)
|
|
154
|
+
return await mediaSection(block.data, block.mimeType ?? "audio/wav", media);
|
|
110
155
|
case "resource": {
|
|
111
156
|
const r = block.resource ?? {};
|
|
112
157
|
// 内嵌文本资源:本质是纯文本,并入正文(同样受输出保护)
|
|
@@ -115,9 +160,7 @@ function toMessageSection(block) {
|
|
|
115
160
|
// 内嵌二进制资源:按 mimeType 选块类型,不丢弃
|
|
116
161
|
if (typeof r.blob === "string") {
|
|
117
162
|
const mime = r.mimeType ?? "application/octet-stream";
|
|
118
|
-
return mime
|
|
119
|
-
? { type: "image_url", image_url: { url: toDataUrl(r.blob, mime) } }
|
|
120
|
-
: { type: "file", file_data: toDataUrl(r.blob, mime) };
|
|
163
|
+
return await mediaSection(r.blob, mime, media);
|
|
121
164
|
}
|
|
122
165
|
return { type: "text", text: `[资源] ${r.uri ?? "(未知 URI)"}` };
|
|
123
166
|
}
|
|
@@ -205,7 +248,7 @@ export function createCallMcpTool(mcpManager, env) {
|
|
|
205
248
|
return new CallbackTool({
|
|
206
249
|
function: {
|
|
207
250
|
name: "call_mcp_tool",
|
|
208
|
-
description: "调用已连接 MCP 服务器上的指定工具。需先通过 load_mcp
|
|
251
|
+
description: "调用已连接 MCP 服务器上的指定工具。需先通过 load_mcp 连接服务器。文本输出过大时会落盘并只返回头尾预览;图片、音频等媒体会保留:端点支持 Files API 时上传后以 file_id 引用,否则 base64 内联。",
|
|
209
252
|
parameters: {
|
|
210
253
|
type: "object",
|
|
211
254
|
properties: {
|
|
@@ -241,7 +284,11 @@ export function createCallMcpTool(mcpManager, env) {
|
|
|
241
284
|
const json = JSON.stringify(result);
|
|
242
285
|
return await protectCallOutput({ sections: [{ type: "text", text: json }], fullText: json, chars: json.length, isError }, env);
|
|
243
286
|
}
|
|
244
|
-
|
|
287
|
+
// 按当前模型定位端点:决定媒体走 Files API 上传还是 base64 内联
|
|
288
|
+
const { model, endpoint } = resolveModelAndEndpoint(env.config, ctx?.agent?.model);
|
|
289
|
+
const mediaContext = { endpoint, model, signal: ctx?.signal };
|
|
290
|
+
// Promise.all 保序:文本与媒体的相对位置不变
|
|
291
|
+
const sections = await Promise.all(contents.map((block) => toMessageSection(block, mediaContext)));
|
|
245
292
|
const textSections = sections.filter((s) => s.type === "text");
|
|
246
293
|
const fullText = textSections.map((s) => s.text).join("\n");
|
|
247
294
|
const chars = textSections.reduce((n, s) => n + s.text.length, 0);
|
|
@@ -256,11 +303,11 @@ export function createCallMcpTool(mcpManager, env) {
|
|
|
256
303
|
/**
|
|
257
304
|
* 创建 read_mcp_resource 工具。
|
|
258
305
|
*/
|
|
259
|
-
export function createReadMcpResourceTool(mcpManager) {
|
|
306
|
+
export function createReadMcpResourceTool(mcpManager, env) {
|
|
260
307
|
return new CallbackTool({
|
|
261
308
|
function: {
|
|
262
309
|
name: "read_mcp_resource",
|
|
263
|
-
description: "读取已连接 MCP
|
|
310
|
+
description: "读取已连接 MCP 服务器上的指定资源(文档、数据、图片等)。文本原样返回;图片等媒体按端点能力上传(file_id)或 base64 内联,不会被丢弃。",
|
|
264
311
|
parameters: {
|
|
265
312
|
type: "object",
|
|
266
313
|
properties: {
|
|
@@ -285,10 +332,22 @@ export function createReadMcpResourceTool(mcpManager) {
|
|
|
285
332
|
mcpManager.touch(serverName);
|
|
286
333
|
const result = await client.readResource({ uri: input.uri }, { signal: ctx?.signal });
|
|
287
334
|
const contents = result?.contents ?? [];
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
335
|
+
const { model, endpoint } = resolveModelAndEndpoint(env.config, ctx?.agent?.model);
|
|
336
|
+
const mediaContext = { endpoint, model, signal: ctx?.signal };
|
|
337
|
+
// 与 call_mcp_tool 同一套映射:文本归一为 text 块,媒体按端点能力上传或内联,
|
|
338
|
+
// 不再把二进制资源(图片 / PDF)静默丢掉。
|
|
339
|
+
const sections = await Promise.all(contents.map((content) => resourceToMessageSection(content, mediaContext)));
|
|
340
|
+
// 没有 contents 时退回原始 JSON(与 call_mcp_tool 同一兜底)
|
|
341
|
+
if (sections.length === 0) {
|
|
342
|
+
const json = JSON.stringify(result);
|
|
343
|
+
return await protectCallOutput({ sections: [{ type: "text", text: json }], fullText: json, chars: json.length, isError: false }, env);
|
|
344
|
+
}
|
|
345
|
+
// 与 call_mcp_tool 共用同一套输出保护:文本超限落盘 result.txt,
|
|
346
|
+
// 媒体块原位保留;未超限时纯文本退化为字符串、含媒体返回块数组。
|
|
347
|
+
const textSections = sections.filter((section) => section.type === "text");
|
|
348
|
+
const fullText = textSections.map((section) => section.text).join("\n");
|
|
349
|
+
const chars = textSections.reduce((n, section) => n + section.text.length, 0);
|
|
350
|
+
return await protectCallOutput({ sections, fullText, chars, isError: false }, env);
|
|
292
351
|
}
|
|
293
352
|
catch (error) {
|
|
294
353
|
return `❌ 读取资源 "${input.uri}" 失败: ${error?.message ?? error}`;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本地文件 → 消息内容块的唯一转换入口。
|
|
3
|
+
*
|
|
4
|
+
* 背景:这段逻辑原先散落三处,且各自为政 ——
|
|
5
|
+
* 1. `ViewImageTool` 自己判断 `endpoint.id === "deepseek"` 再上传(端点 id 一变就失效);
|
|
6
|
+
* 2. `mcp/mcpTools.ts` 自己把 MCP 的 base64 归一成 `image_url` / `file` 块;
|
|
7
|
+
* 3. CLI 侧又实现了一遍「上传 or base64」。
|
|
8
|
+
*
|
|
9
|
+
* 现在统一到这里,调用方只提供「文件 + 当前端点/模型」,传输方式由本模块决定:
|
|
10
|
+
*
|
|
11
|
+
* | 条件 | 结果 |
|
|
12
|
+
* |------|------|
|
|
13
|
+
* | http(s) URL | 直接 `image_url`(不上传、不内联) |
|
|
14
|
+
* | 端点声明 `capabilities.filesApi` | 上传 Files API → `file` 块(`file_id`),失败降级为内联 |
|
|
15
|
+
* | 其余端点 + 图片 | `image_url` + base64 data URL |
|
|
16
|
+
* | 其余端点 + 非图片 | `file` 块 + `file_data`(base64 data URL) |
|
|
17
|
+
* | 模型明确不支持视觉(`vision === false`)且是图片 | 退化为 `text`,只给路径提示 |
|
|
18
|
+
* | 内联体积超过上限且无法上传 | 抛错(不静默塞进请求体) |
|
|
19
|
+
*/
|
|
20
|
+
import type { AgentNS } from "@ai-zen/agents-core";
|
|
21
|
+
import type { AppConfig, Endpoint, Model } from "../types/index.js";
|
|
22
|
+
/** 扩展名 → MIME。图片走视觉通道,其余走 `file` 块 */
|
|
23
|
+
export declare const MIME_BY_EXT: Record<string, string>;
|
|
24
|
+
/** 按扩展名推断 MIME;未知扩展名返回 undefined */
|
|
25
|
+
export declare function mimeOf(filePath: string): string | undefined;
|
|
26
|
+
/** base64 二进制转 data URL(`image_url` 与 `file.file_data` 均接受) */
|
|
27
|
+
export declare function toDataUrl(base64: string, mime: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* base64 字符串对应的字节数(不含空白)。
|
|
30
|
+
*
|
|
31
|
+
* 用于「只看体积、不想解码」的场景:内联上限检查与日志统计。
|
|
32
|
+
*/
|
|
33
|
+
export declare function base64ByteLength(base64: string): number;
|
|
34
|
+
/** 内联(base64)体积上限,默认 8 MiB;超过且端点不支持上传时直接报错 */
|
|
35
|
+
export declare const DEFAULT_MAX_INLINE_BYTES: number;
|
|
36
|
+
/** 实际采用的传输方式(供调用方组织提示文案 / 日志) */
|
|
37
|
+
export type ContentTransport = "url" | "files-api" | "inline" | "text";
|
|
38
|
+
/** 文件来源:本地路径 / 网络 URL / 已在内存里的字节 */
|
|
39
|
+
export type FileSource = {
|
|
40
|
+
path: string;
|
|
41
|
+
} | {
|
|
42
|
+
url: string;
|
|
43
|
+
} | {
|
|
44
|
+
data: Uint8Array;
|
|
45
|
+
mime: string;
|
|
46
|
+
filename?: string;
|
|
47
|
+
}
|
|
48
|
+
/** 已经是 base64 的字节(MCP 等协议给的就是 base64):内联时原样透传,不做解码再编码 */
|
|
49
|
+
| {
|
|
50
|
+
base64: string;
|
|
51
|
+
mime: string;
|
|
52
|
+
filename?: string;
|
|
53
|
+
};
|
|
54
|
+
/** Files API 上传函数签名(可注入,便于测试与替换实现) */
|
|
55
|
+
export type UploadFn = (input: {
|
|
56
|
+
endpoint: Endpoint;
|
|
57
|
+
data: Uint8Array;
|
|
58
|
+
filename: string;
|
|
59
|
+
signal?: AbortSignal;
|
|
60
|
+
}) => Promise<string>;
|
|
61
|
+
export interface FileToSectionInput {
|
|
62
|
+
source: FileSource;
|
|
63
|
+
/** 当前模型所用端点:决定能否走 Files API */
|
|
64
|
+
endpoint?: Endpoint | null;
|
|
65
|
+
/** 当前模型:`vision === false` 时图片退化为文本提示 */
|
|
66
|
+
model?: Model | null;
|
|
67
|
+
signal?: AbortSignal;
|
|
68
|
+
/** 覆盖内联体积上限 */
|
|
69
|
+
maxInlineBytes?: number;
|
|
70
|
+
/** 依赖注入(测试用),默认走 OpenAI SDK 的 Files API */
|
|
71
|
+
deps?: {
|
|
72
|
+
upload?: UploadFn;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export interface FileToSectionResult {
|
|
76
|
+
section: AgentNS.MessageContentSection;
|
|
77
|
+
transport: ContentTransport;
|
|
78
|
+
/** 实际处理的字节数(URL 来源为 0) */
|
|
79
|
+
bytes: number;
|
|
80
|
+
/** 降级 / 退化时的人类可读说明,供上层拼提示 */
|
|
81
|
+
warning?: string;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 由「发给 API 的模型名」反查它绑定的模型定义与端点。
|
|
85
|
+
*
|
|
86
|
+
* 工具在 `call()` 阶段拿不到 Agent 定义,但 `ctx.agent.model` 就是发给 API 的
|
|
87
|
+
* 模型名(`Model.modelName ?? Model.id`)。必须按**当前实际使用的模型**定位端点:
|
|
88
|
+
* `file_id` 是端点维度资源,传错端点会得到 not found。
|
|
89
|
+
*/
|
|
90
|
+
export declare function resolveModelAndEndpoint(config: AppConfig, apiModelName?: string): {
|
|
91
|
+
model?: Model;
|
|
92
|
+
endpoint?: Endpoint | null;
|
|
93
|
+
};
|
|
94
|
+
/** 端点是否声明了 Files API 能力 */
|
|
95
|
+
export declare function supportsFilesApi(endpoint?: Endpoint | null): boolean;
|
|
96
|
+
/** 清空上传缓存(测试用;进程内长期运行无需清理,条目有上限) */
|
|
97
|
+
export declare function clearUploadCache(): void;
|
|
98
|
+
/** 内容哈希(sha256),用于给「内存里的字节」做上传缓存键 */
|
|
99
|
+
export declare function contentHash(bytes: Uint8Array): string;
|
|
100
|
+
/**
|
|
101
|
+
* 把文件转成内容块。
|
|
102
|
+
*
|
|
103
|
+
* 抛错场景(均由调用方决定如何呈现给模型):
|
|
104
|
+
* - `source.url` 不是 http(s)(本地 URL 服务商访问不到)
|
|
105
|
+
* - 本地文件不存在 / 不是文件 / 扩展名不支持
|
|
106
|
+
* - 需要内联但体积超过上限,且端点不支持上传
|
|
107
|
+
*/
|
|
108
|
+
export declare function fileToContentSection(input: FileToSectionInput): Promise<FileToSectionResult>;
|