@nickyzj2023/ai 1.4.4 → 1.5.0
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/dist/cli.mjs +161 -21
- package/dist/index.d.mts +11 -2
- package/dist/index.mjs +1 -1
- package/dist/{src-CuZ-6Jye.mjs → src-C1Zeveeb.mjs} +109 -81
- package/package.json +2 -2
package/dist/cli.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { a as get_time_default,
|
|
1
|
+
import { a as get_time_default, i as get_weather_default, o as runAgent, r as loadMCPTools, s as defineModel } from "./src-C1Zeveeb.mjs";
|
|
2
2
|
import { compactStr, extractErrorMessage, isObject } from "@nickyzj2023/utils";
|
|
3
3
|
import readline from "node:readline";
|
|
4
4
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
import { MarkdownRenderer } from "@wterm/markdown";
|
|
8
|
+
import { execFile } from "node:child_process";
|
|
8
9
|
//#region src/utils/config.ts
|
|
9
10
|
/**
|
|
10
11
|
* 获取全局配置文件路径(Windows/macOS/Linux通用)
|
|
@@ -197,10 +198,110 @@ async function runSetup() {
|
|
|
197
198
|
}
|
|
198
199
|
}
|
|
199
200
|
//#endregion
|
|
201
|
+
//#region src/utils/clipboard.ts
|
|
202
|
+
/** 剪贴板图片的原始字节上限:超过则拒绝,避免把超大图片塞入请求体 */
|
|
203
|
+
const MAX_CLIPBOARD_IMAGE_BYTES = 5242880;
|
|
204
|
+
/**
|
|
205
|
+
* 执行命令并原样取回stdout(二进制安全)
|
|
206
|
+
* @param maxBuffer 允许的最大输出字节数
|
|
207
|
+
*/
|
|
208
|
+
const run = (command, args, maxBuffer) => {
|
|
209
|
+
return new Promise((resolve, reject) => {
|
|
210
|
+
execFile(command, args, {
|
|
211
|
+
encoding: "buffer",
|
|
212
|
+
timeout: 5e3,
|
|
213
|
+
maxBuffer
|
|
214
|
+
}, (error, stdout) => {
|
|
215
|
+
if (error) reject(error);
|
|
216
|
+
else resolve(stdout);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
};
|
|
220
|
+
/** 组装成OpenAI接口能接受的data URL */
|
|
221
|
+
const toDataUrl = (buffer, mime = "image/png") => ({
|
|
222
|
+
url: `data:${mime};base64,${buffer.toString("base64")}`,
|
|
223
|
+
bytes: buffer.length
|
|
224
|
+
});
|
|
225
|
+
/** Windows:用PowerShell把剪贴板里的图片转成base64后读出 */
|
|
226
|
+
const readWindows = async () => {
|
|
227
|
+
const script = [
|
|
228
|
+
"Add-Type -AssemblyName System.Windows.Forms",
|
|
229
|
+
"Add-Type -AssemblyName System.Drawing",
|
|
230
|
+
"$img = [System.Windows.Forms.Clipboard]::GetImage()",
|
|
231
|
+
"if ($img) {",
|
|
232
|
+
"$ms = New-Object System.IO.MemoryStream",
|
|
233
|
+
"$img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)",
|
|
234
|
+
"[Convert]::ToBase64String($ms.ToArray())",
|
|
235
|
+
"}"
|
|
236
|
+
].join("; ");
|
|
237
|
+
const base64 = (await run("powershell.exe", [
|
|
238
|
+
"-NoProfile",
|
|
239
|
+
"-Command",
|
|
240
|
+
script
|
|
241
|
+
], 67108864)).toString("utf8").trim();
|
|
242
|
+
return base64 ? toDataUrl(Buffer.from(base64, "base64")) : null;
|
|
243
|
+
};
|
|
244
|
+
/** Linux:Wayland优先,其次X11,最后用python3的tkinter兜底;命令未安装或剪贴板里不是图片时跳过 */
|
|
245
|
+
const readLinux = async () => {
|
|
246
|
+
for (const candidate of [{
|
|
247
|
+
command: "wl-paste",
|
|
248
|
+
args: [
|
|
249
|
+
"--no-newline",
|
|
250
|
+
"--type",
|
|
251
|
+
"image/png"
|
|
252
|
+
]
|
|
253
|
+
}, {
|
|
254
|
+
command: "xclip",
|
|
255
|
+
args: [
|
|
256
|
+
"-selection",
|
|
257
|
+
"clipboard",
|
|
258
|
+
"-t",
|
|
259
|
+
"image/png",
|
|
260
|
+
"-o"
|
|
261
|
+
]
|
|
262
|
+
}]) try {
|
|
263
|
+
const stdout = await run(candidate.command, candidate.args, MAX_CLIPBOARD_IMAGE_BYTES * 2);
|
|
264
|
+
if (stdout.length === 0) continue;
|
|
265
|
+
return candidate.base64 ? toDataUrl(Buffer.from(stdout.toString("ascii").trim(), "base64")) : toDataUrl(stdout);
|
|
266
|
+
} catch {}
|
|
267
|
+
return null;
|
|
268
|
+
};
|
|
269
|
+
/**
|
|
270
|
+
* 读取系统剪贴板里的图片
|
|
271
|
+
* @returns 剪贴板里没有图片、或当前环境拿不到时返回null
|
|
272
|
+
*/
|
|
273
|
+
const readClipboardImage = async () => {
|
|
274
|
+
try {
|
|
275
|
+
switch (process.platform) {
|
|
276
|
+
case "win32": return await readWindows();
|
|
277
|
+
default: return await readLinux();
|
|
278
|
+
}
|
|
279
|
+
} catch {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
//#endregion
|
|
284
|
+
//#region src/utils/internalHelper.ts
|
|
285
|
+
const formatter = new Intl.NumberFormat("en-US", {
|
|
286
|
+
notation: "compact",
|
|
287
|
+
maximumFractionDigits: 1
|
|
288
|
+
});
|
|
289
|
+
/** 把数字转换成千分位 */
|
|
290
|
+
const humanizeNumber = (number) => {
|
|
291
|
+
return formatter.format(number);
|
|
292
|
+
};
|
|
293
|
+
//#endregion
|
|
200
294
|
//#region src/interfaces/tui.ts
|
|
201
295
|
var TUI = class {
|
|
202
296
|
rl = null;
|
|
203
297
|
onPrompt = null;
|
|
298
|
+
/**
|
|
299
|
+
* 已粘贴、等待随下次请求一起发出的图片
|
|
300
|
+
* TODO: 改为pendingMedias
|
|
301
|
+
*/
|
|
302
|
+
pendingImages = [];
|
|
303
|
+
/** 剪贴板读取是异步的,回车时要先等它结束,避免漏掉刚粘贴的图片 */
|
|
304
|
+
clipboardTask = Promise.resolve();
|
|
204
305
|
/** content专用的markdown渲染器 */
|
|
205
306
|
md = null;
|
|
206
307
|
/** 是否允许输入 */
|
|
@@ -222,27 +323,57 @@ var TUI = class {
|
|
|
222
323
|
input: process.stdin,
|
|
223
324
|
output: process.stdout
|
|
224
325
|
});
|
|
326
|
+
process.stdin.on("keypress", this.onKeypress);
|
|
225
327
|
this.prompting();
|
|
226
328
|
this.rl.on("close", () => {
|
|
329
|
+
process.stdin.off("keypress", this.onKeypress);
|
|
227
330
|
this.rl?.close();
|
|
228
331
|
this.rl = null;
|
|
229
332
|
});
|
|
230
333
|
}
|
|
334
|
+
/** 监听用户的特定按键 */
|
|
335
|
+
onKeypress = (_str, key) => {
|
|
336
|
+
if (this.isBusy) return;
|
|
337
|
+
if (key?.ctrl && key?.name === "v") this.clipboardTask = this.clipboardTask.then(() => this.attachImage());
|
|
338
|
+
};
|
|
231
339
|
/** 监听用户输入 */
|
|
232
340
|
prompting() {
|
|
233
341
|
if (this.isBusy) return;
|
|
234
342
|
this.rl?.question("> ", async (answer) => {
|
|
235
|
-
|
|
236
|
-
|
|
343
|
+
await this.clipboardTask;
|
|
344
|
+
const text = answer.trim();
|
|
345
|
+
const images = this.pendingImages;
|
|
346
|
+
this.pendingImages = [];
|
|
347
|
+
if (!text && images.length === 0) {
|
|
237
348
|
this.prompting();
|
|
238
349
|
return;
|
|
239
350
|
}
|
|
240
351
|
this.isBusy = true;
|
|
241
|
-
await this.onPrompt?.(
|
|
352
|
+
await this.onPrompt?.({
|
|
353
|
+
text,
|
|
354
|
+
images
|
|
355
|
+
});
|
|
242
356
|
this.isBusy = false;
|
|
243
357
|
this.prompting();
|
|
244
358
|
});
|
|
245
359
|
}
|
|
360
|
+
/** 读取剪贴板图片,推入待发送列表 */
|
|
361
|
+
async attachImage() {
|
|
362
|
+
const image = await readClipboardImage();
|
|
363
|
+
if (!image) {
|
|
364
|
+
this.printNotice("[剪贴板中没有图片]");
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (image.bytes > 5242880) {
|
|
368
|
+
this.printNotice(`[图片过大(${humanizeNumber(image.bytes)}B),已忽略]`);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
this.pendingImages.push({
|
|
372
|
+
type: "image_url",
|
|
373
|
+
image_url: { url: image.url }
|
|
374
|
+
});
|
|
375
|
+
this.printNotice(`[已粘贴图片:共${this.pendingImages.length}张]`);
|
|
376
|
+
}
|
|
246
377
|
/**
|
|
247
378
|
* 所有print方法调用前:
|
|
248
379
|
* 状态切换时打印换行,并记录当前状态
|
|
@@ -294,27 +425,33 @@ var TUI = class {
|
|
|
294
425
|
this.preparePrint("tool_result");
|
|
295
426
|
process.stdout.write(this.colorize(`[工具结果:${name}] ${_result}\n`, "90"));
|
|
296
427
|
}
|
|
297
|
-
formatter = new Intl.NumberFormat("en-US", {
|
|
298
|
-
notation: "compact",
|
|
299
|
-
maximumFractionDigits: 1
|
|
300
|
-
});
|
|
301
|
-
format(number) {
|
|
302
|
-
return this.formatter.format(number);
|
|
303
|
-
}
|
|
304
428
|
/** 打印轮次结束原因、token消耗 */
|
|
305
429
|
printFinish(finishReason, usage) {
|
|
306
430
|
this.preparePrint("done");
|
|
307
|
-
process.stdout.write(this.colorize(`[本轮结束:${finishReason}] ${usage ? `输入${
|
|
431
|
+
process.stdout.write(this.colorize(`[本轮结束:${finishReason}] ${usage ? `输入${humanizeNumber(usage.prompt_tokens)},输出${humanizeNumber(usage.completion_tokens)},总共${humanizeNumber(usage.total_tokens)}` : ""}${usage?.speed ? `,速度${usage.speed}/秒` : ""}${finishReason === "stop" ? "\n\n" : "\n"}`, "90"));
|
|
308
432
|
}
|
|
309
|
-
/**
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
process.stdout.write(this.colorize(message, "93"));
|
|
433
|
+
/** 打印一行黄字提示,并重绘当前输入行 */
|
|
434
|
+
printNotice(message) {
|
|
435
|
+
process.stdout.write(`${this.colorize(message, "93")}\n`);
|
|
436
|
+
this.rl?.prompt(true);
|
|
314
437
|
}
|
|
315
438
|
};
|
|
316
439
|
//#endregion
|
|
317
440
|
//#region src/cli.ts
|
|
441
|
+
/**
|
|
442
|
+
* 组装用户消息的content
|
|
443
|
+
* @remarks 有图片时必须用ContentPart[](OpenAI多模态协议),纯文本则保持字符串
|
|
444
|
+
*/
|
|
445
|
+
const buildUserContent = (text, images) => {
|
|
446
|
+
if (images.length === 0) return text;
|
|
447
|
+
const content = [];
|
|
448
|
+
if (text) content.push({
|
|
449
|
+
type: "text",
|
|
450
|
+
text
|
|
451
|
+
});
|
|
452
|
+
content.push(...images);
|
|
453
|
+
return content;
|
|
454
|
+
};
|
|
318
455
|
/** 启动交互对话:配置来自全局配置文件,环境变量可临时覆盖 */
|
|
319
456
|
const startChat = async () => {
|
|
320
457
|
const config = loadConfig();
|
|
@@ -328,14 +465,17 @@ const startChat = async () => {
|
|
|
328
465
|
mcpReady = true;
|
|
329
466
|
return tools;
|
|
330
467
|
});
|
|
331
|
-
const messages = [
|
|
468
|
+
const messages = [{
|
|
469
|
+
role: "system",
|
|
470
|
+
content: `你是${model.model},当前时间${(/* @__PURE__ */ new Date()).toLocaleString()}`
|
|
471
|
+
}];
|
|
332
472
|
let tools = null;
|
|
333
|
-
const tui = new TUI(async (
|
|
473
|
+
const tui = new TUI(async ({ text, images }) => {
|
|
334
474
|
messages.push({
|
|
335
475
|
role: "user",
|
|
336
|
-
content:
|
|
476
|
+
content: buildUserContent(text, images)
|
|
337
477
|
});
|
|
338
|
-
if (!mcpReady) tui.
|
|
478
|
+
if (!mcpReady) tui.printNotice("MCP工具还未加载完成,请稍候……");
|
|
339
479
|
tools ??= [
|
|
340
480
|
get_weather_default,
|
|
341
481
|
get_time_default,
|
package/dist/index.d.mts
CHANGED
|
@@ -131,6 +131,8 @@ type Usage = {
|
|
|
131
131
|
prompt_tokens: number;
|
|
132
132
|
completion_tokens: number;
|
|
133
133
|
total_tokens: number;
|
|
134
|
+
/** 每秒生成多少个token */
|
|
135
|
+
speed?: number;
|
|
134
136
|
[key: string]: any;
|
|
135
137
|
};
|
|
136
138
|
type AgentEvent = LLMEvent | {
|
|
@@ -249,8 +251,15 @@ declare namespace Compact {
|
|
|
249
251
|
*/
|
|
250
252
|
summarizeOptions?: Partial<Compact.SummarizeOptions>;
|
|
251
253
|
};
|
|
252
|
-
type ReplacerOfToolResultContent = (content: Message["content"], options?:
|
|
253
|
-
|
|
254
|
+
type ReplacerOfToolResultContent = (content: Message["content"], options?: {
|
|
255
|
+
model?: Model;
|
|
256
|
+
[key: string]: any;
|
|
257
|
+
}) => Promise<string> | string;
|
|
258
|
+
type ReplacerOfMediaContent = (content: Message["content"], options?: {
|
|
259
|
+
model?: Model;
|
|
260
|
+
modality?: Modality;
|
|
261
|
+
[key: string]: any;
|
|
262
|
+
}) => Promise<string> | string;
|
|
254
263
|
type SummarizeOptions = {
|
|
255
264
|
/** 指导大模型如何总结消息 */
|
|
256
265
|
systemPrompt: string;
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as get_time_default, c as
|
|
1
|
+
import { a as get_time_default, c as defineTool, i as get_weather_default, n as MCPRouter, o as runAgent, r as loadMCPTools, s as defineModel, t as compact } from "./src-C1Zeveeb.mjs";
|
|
2
2
|
export { MCPRouter, compact, defineModel, defineTool, get_time_default as getTime, get_weather_default as getWeather, loadMCPTools, runAgent };
|
|
@@ -91,64 +91,6 @@ async function* stream(model, messages, tools = []) {
|
|
|
91
91
|
};
|
|
92
92
|
}
|
|
93
93
|
//#endregion
|
|
94
|
-
//#region src/agent.ts
|
|
95
|
-
async function* runAgent(model, messages, tools) {
|
|
96
|
-
const toolMap = new Map(tools.map((tool) => [tool.function.name, tool]));
|
|
97
|
-
while (true) {
|
|
98
|
-
let content = "";
|
|
99
|
-
const toolCalls = [];
|
|
100
|
-
for await (const e of stream(model, messages, tools)) switch (e.type) {
|
|
101
|
-
case "reasoning_delta":
|
|
102
|
-
yield e;
|
|
103
|
-
break;
|
|
104
|
-
case "content_delta":
|
|
105
|
-
content += e.delta;
|
|
106
|
-
yield e;
|
|
107
|
-
break;
|
|
108
|
-
case "tool_call":
|
|
109
|
-
toolCalls.push({
|
|
110
|
-
id: e.id,
|
|
111
|
-
type: "function",
|
|
112
|
-
function: {
|
|
113
|
-
name: e.name,
|
|
114
|
-
arguments: e.args
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
yield e;
|
|
118
|
-
break;
|
|
119
|
-
case "done": yield e;
|
|
120
|
-
}
|
|
121
|
-
const message = {
|
|
122
|
-
role: "assistant",
|
|
123
|
-
content
|
|
124
|
-
};
|
|
125
|
-
if (toolCalls.length > 0) message.tool_calls = toolCalls;
|
|
126
|
-
messages.push(message);
|
|
127
|
-
if (toolCalls.length === 0) return;
|
|
128
|
-
for (const call of toolCalls) {
|
|
129
|
-
const { name, arguments: args } = call.function;
|
|
130
|
-
const tool = toolMap.get(name);
|
|
131
|
-
let result = "";
|
|
132
|
-
if (!tool) result = `不存在工具“${name}”`;
|
|
133
|
-
else {
|
|
134
|
-
const [error, response] = await to(tool.execute(JSON.parse(args)));
|
|
135
|
-
result = error ? `工具“${name}”执行出错:${error.message}` : JSON.stringify(response);
|
|
136
|
-
}
|
|
137
|
-
messages.push({
|
|
138
|
-
role: "tool",
|
|
139
|
-
tool_call_id: call.id,
|
|
140
|
-
content: result
|
|
141
|
-
});
|
|
142
|
-
yield {
|
|
143
|
-
type: "tool_result",
|
|
144
|
-
id: call.id,
|
|
145
|
-
name,
|
|
146
|
-
result
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
//#endregion
|
|
152
94
|
//#region src/utils/helper.ts
|
|
153
95
|
/**
|
|
154
96
|
* 辅助定义一个POST /chat/completions支持的model参数
|
|
@@ -186,22 +128,22 @@ const defineTool = (name, description, properties, execute) => {
|
|
|
186
128
|
execute
|
|
187
129
|
};
|
|
188
130
|
};
|
|
131
|
+
const segmenter = new Intl.Segmenter([], { granularity: "word" });
|
|
132
|
+
const estimateTextTokens = (text) => {
|
|
133
|
+
let words = 0;
|
|
134
|
+
let others = 0;
|
|
135
|
+
for (const seg of segmenter.segment(text)) if (seg.isWordLike) words++;
|
|
136
|
+
else others++;
|
|
137
|
+
return Math.ceil(words * 1.5 + others / 4);
|
|
138
|
+
};
|
|
189
139
|
/**
|
|
190
140
|
* 根据上下文里的中/英文/多模态消息,估算出可能消耗的token
|
|
191
141
|
* - 单词 ≈ 1.5token
|
|
192
|
-
* - 标点/空白等非词字符每
|
|
142
|
+
* - 标点/空白等非词字符每4个 ≈ 1token
|
|
193
143
|
* - 图片/音频/视频/文件 ≈ 4096token(不好估算,取个较大的值)
|
|
194
144
|
*/
|
|
195
145
|
const estimateTokens = (messages) => {
|
|
196
146
|
if (!messages?.length) return 0;
|
|
197
|
-
const segmenter = new Intl.Segmenter([], { granularity: "word" });
|
|
198
|
-
const estimateTextTokens = (text) => {
|
|
199
|
-
let words = 0;
|
|
200
|
-
let others = 0;
|
|
201
|
-
for (const seg of segmenter.segment(text)) if (seg.isWordLike) words++;
|
|
202
|
-
else others++;
|
|
203
|
-
return Math.ceil(words * 1.5 + others / 4);
|
|
204
|
-
};
|
|
205
147
|
return messages.reduce((acc, message) => {
|
|
206
148
|
const { content, tool_calls, ...metadata } = message;
|
|
207
149
|
if (typeof content === "string") acc += estimateTextTokens(content);
|
|
@@ -213,6 +155,85 @@ const estimateTokens = (messages) => {
|
|
|
213
155
|
}, 0);
|
|
214
156
|
};
|
|
215
157
|
//#endregion
|
|
158
|
+
//#region src/agent.ts
|
|
159
|
+
async function* runAgent(model, messages, tools) {
|
|
160
|
+
const toolMap = new Map(tools.map((tool) => [tool.function.name, tool]));
|
|
161
|
+
while (true) {
|
|
162
|
+
let content = "";
|
|
163
|
+
let reasoning = "";
|
|
164
|
+
const toolCalls = [];
|
|
165
|
+
const promptMessage = messages.slice(-1);
|
|
166
|
+
let startTime = 0;
|
|
167
|
+
for await (const e of stream(model, messages, tools)) {
|
|
168
|
+
startTime ||= Date.now();
|
|
169
|
+
switch (e.type) {
|
|
170
|
+
case "reasoning_delta":
|
|
171
|
+
reasoning += e.delta;
|
|
172
|
+
yield e;
|
|
173
|
+
break;
|
|
174
|
+
case "content_delta":
|
|
175
|
+
content += e.delta;
|
|
176
|
+
yield e;
|
|
177
|
+
break;
|
|
178
|
+
case "tool_call":
|
|
179
|
+
toolCalls.push({
|
|
180
|
+
id: e.id,
|
|
181
|
+
type: "function",
|
|
182
|
+
function: {
|
|
183
|
+
name: e.name,
|
|
184
|
+
arguments: e.args
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
yield e;
|
|
188
|
+
break;
|
|
189
|
+
case "error":
|
|
190
|
+
yield e;
|
|
191
|
+
break;
|
|
192
|
+
case "done":
|
|
193
|
+
if (!e.usage) {
|
|
194
|
+
const promptTokens = estimateTokens(promptMessage);
|
|
195
|
+
const completionTokens = estimateTextTokens(content + reasoning);
|
|
196
|
+
e.usage = {
|
|
197
|
+
prompt_tokens: promptTokens,
|
|
198
|
+
completion_tokens: completionTokens,
|
|
199
|
+
total_tokens: estimateTokens(messages) + promptTokens + completionTokens
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
e.usage.speed = Math.floor(e.usage.completion_tokens / ((Date.now() - startTime) / 1e3));
|
|
203
|
+
yield e;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const message = {
|
|
207
|
+
role: "assistant",
|
|
208
|
+
content
|
|
209
|
+
};
|
|
210
|
+
if (toolCalls.length > 0) message.tool_calls = toolCalls;
|
|
211
|
+
messages.push(message);
|
|
212
|
+
if (toolCalls.length === 0) return;
|
|
213
|
+
for (const call of toolCalls) {
|
|
214
|
+
const { name, arguments: args } = call.function;
|
|
215
|
+
const tool = toolMap.get(name);
|
|
216
|
+
let result = "";
|
|
217
|
+
if (!tool) result = `不存在工具“${name}”`;
|
|
218
|
+
else {
|
|
219
|
+
const [error, response] = await to(tool.execute(JSON.parse(args)));
|
|
220
|
+
result = error ? `工具“${name}”执行出错:${error.message}` : JSON.stringify(response);
|
|
221
|
+
}
|
|
222
|
+
messages.push({
|
|
223
|
+
role: "tool",
|
|
224
|
+
tool_call_id: call.id,
|
|
225
|
+
content: result
|
|
226
|
+
});
|
|
227
|
+
yield {
|
|
228
|
+
type: "tool_result",
|
|
229
|
+
id: call.id,
|
|
230
|
+
name,
|
|
231
|
+
result
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
//#endregion
|
|
216
237
|
//#region src/tools/get-time.ts
|
|
217
238
|
var get_time_default = defineTool("get_time", "查询指定时区的当前时间", { timezone: {
|
|
218
239
|
type: "string",
|
|
@@ -296,12 +317,11 @@ const isToolCalls = (message) => {
|
|
|
296
317
|
* 校验多模态消息
|
|
297
318
|
*/
|
|
298
319
|
const isMediaMessage = (message) => {
|
|
299
|
-
const
|
|
300
|
-
"image_url"
|
|
301
|
-
"input_audio"
|
|
302
|
-
"video_url"
|
|
303
|
-
|
|
304
|
-
return message && Array.isArray(message.content) && message.content.some((part) => MEDIA_TYPES.includes(part.type));
|
|
320
|
+
if (message && Array.isArray(message.content)) for (const part of message.content) {
|
|
321
|
+
if (part.type === "image_url") return "image";
|
|
322
|
+
if (part.type === "input_audio") return "audio";
|
|
323
|
+
if (part.type === "video_url") return "video";
|
|
324
|
+
}
|
|
305
325
|
};
|
|
306
326
|
/**
|
|
307
327
|
* 查找assistant(tool_calls) + tool配对组范围
|
|
@@ -330,7 +350,8 @@ const defaultReplacerOfToolResultContent = async (content, options) => {
|
|
|
330
350
|
content: "请用一句话简述上面这条消息(直击要点,不要添加任何前言后语)"
|
|
331
351
|
}];
|
|
332
352
|
let simplifiedContent = "";
|
|
333
|
-
|
|
353
|
+
if (!model) simplifiedContent = "工具结果已被消费";
|
|
354
|
+
else for await (const e of runAgent(model, messages, [])) if (e.type === "content_delta") simplifiedContent += e.delta;
|
|
334
355
|
else if (e.type === "error") throw new Error(e.message);
|
|
335
356
|
return simplifiedContent;
|
|
336
357
|
};
|
|
@@ -352,7 +373,7 @@ const compactToolResults = async (compressible, options) => {
|
|
|
352
373
|
};
|
|
353
374
|
/** 默认的压缩多模态消息策略:让大模型精简消息内容 */
|
|
354
375
|
const defaultReplacerOfMediaContent = async (content, options) => {
|
|
355
|
-
const { model } = options ?? {};
|
|
376
|
+
const { model, modality = "image" } = options ?? {};
|
|
356
377
|
const messages = [{
|
|
357
378
|
role: "user",
|
|
358
379
|
content
|
|
@@ -361,7 +382,8 @@ const defaultReplacerOfMediaContent = async (content, options) => {
|
|
|
361
382
|
content: "请用一句话简述上面这条消息(直击要点,不要添加任何前言后语)"
|
|
362
383
|
}];
|
|
363
384
|
let simplifiedContent = "";
|
|
364
|
-
|
|
385
|
+
if (!model?.modalities?.includes(modality)) simplifiedContent = "媒体资源已被消费";
|
|
386
|
+
else for await (const e of runAgent(model, messages, [])) if (e.type === "content_delta") simplifiedContent += e.delta;
|
|
365
387
|
else if (e.type === "error") throw new Error(e.message);
|
|
366
388
|
return simplifiedContent;
|
|
367
389
|
};
|
|
@@ -373,10 +395,16 @@ const compactMediaMessages = async (compressible, options) => {
|
|
|
373
395
|
if (!replacer && !model) return 0;
|
|
374
396
|
const _replacer = replacer || defaultReplacerOfMediaContent;
|
|
375
397
|
let count = 0;
|
|
376
|
-
for (const message of compressible)
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
398
|
+
for (const message of compressible) {
|
|
399
|
+
const mediaModality = isMediaMessage(message);
|
|
400
|
+
if (mediaModality) {
|
|
401
|
+
const compacted = await _replacer(message.content, {
|
|
402
|
+
model,
|
|
403
|
+
modality: mediaModality
|
|
404
|
+
});
|
|
405
|
+
message.content = createXMLText("media", compacted);
|
|
406
|
+
count++;
|
|
407
|
+
}
|
|
380
408
|
}
|
|
381
409
|
if (count > 0) logger(`压缩了${count}条多模态消息`);
|
|
382
410
|
return count;
|
|
@@ -484,4 +512,4 @@ const compact = Object.assign(async (messages, model, options) => {
|
|
|
484
512
|
discardMessagesUntil
|
|
485
513
|
});
|
|
486
514
|
//#endregion
|
|
487
|
-
export { get_time_default as a,
|
|
515
|
+
export { get_time_default as a, defineTool as c, get_weather_default as i, MCPRouter as n, runAgent as o, loadMCPTools as r, defineModel as s, compact as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nickyzj2023/ai",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "我的“pi”,参考了pi-from-scratch",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.mjs",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@biomejs/biome": "^2.5.12",
|
|
30
|
-
"@types/node": "^26.
|
|
30
|
+
"@types/node": "^26.5.0",
|
|
31
31
|
"tsdown": "^0.22.14",
|
|
32
32
|
"typescript": "^7.0.2"
|
|
33
33
|
},
|