@nickyzj2023/ai 1.6.0 → 1.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/cli.mjs +97 -49
- package/dist/{helper-juLVGxlQ.mjs → helper-DJw58Gmz.mjs} +13 -1
- package/dist/index.d.mts +9 -1
- package/dist/index.mjs +3 -3
- package/dist/{mcp-DcntQg-a.mjs → mcp-BBjcb8es.mjs} +1 -1
- package/dist/{src-CKKPKPMG.mjs → src-DPR9YjB3.mjs} +2 -2
- package/dist/tools/mcp.mjs +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/cli.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { i as runAgent, n as get_weather_default, r as get_time_default } from "./src-
|
|
2
|
-
import { t as defineModel } from "./helper-
|
|
3
|
-
import { n as loadMCPTools } from "./mcp-
|
|
4
|
-
import { compactStr, extractErrorMessage, isObject } from "@nickyzj2023/utils";
|
|
1
|
+
import { i as runAgent, n as get_weather_default, r as get_time_default } from "./src-DPR9YjB3.mjs";
|
|
2
|
+
import { a as listModels, t as defineModel } from "./helper-DJw58Gmz.mjs";
|
|
3
|
+
import { n as loadMCPTools } from "./mcp-BBjcb8es.mjs";
|
|
4
|
+
import { compactStr, extractErrorMessage, isObject, to } from "@nickyzj2023/utils";
|
|
5
5
|
import readline from "node:readline";
|
|
6
6
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { homedir } from "node:os";
|
|
@@ -110,12 +110,12 @@ const askMcpServer = async (current) => {
|
|
|
110
110
|
};
|
|
111
111
|
};
|
|
112
112
|
/**
|
|
113
|
-
*
|
|
113
|
+
* 把菜单输入的数字解析成对应的名称
|
|
114
114
|
* @param input 用户输入的数字串
|
|
115
|
-
* @param names
|
|
115
|
+
* @param names 当前菜单的名称列表
|
|
116
116
|
* @returns 对应名称;非整数或越界返回undefined
|
|
117
117
|
*/
|
|
118
|
-
const
|
|
118
|
+
const pickByIndex = (input, names) => {
|
|
119
119
|
const index = Number(input) - 1;
|
|
120
120
|
return Number.isInteger(index) ? names[index] : void 0;
|
|
121
121
|
};
|
|
@@ -141,7 +141,7 @@ const configMcp = async (mcpServers) => {
|
|
|
141
141
|
}
|
|
142
142
|
case "d": {
|
|
143
143
|
const target = (await ask("输入要删除的服务器编号: ")).trim();
|
|
144
|
-
const oldName =
|
|
144
|
+
const oldName = pickByIndex(target, names);
|
|
145
145
|
if (!oldName) {
|
|
146
146
|
console.log("无效编号,请输入列表中的数字");
|
|
147
147
|
break;
|
|
@@ -153,7 +153,7 @@ const configMcp = async (mcpServers) => {
|
|
|
153
153
|
break;
|
|
154
154
|
}
|
|
155
155
|
default: {
|
|
156
|
-
const oldName =
|
|
156
|
+
const oldName = pickByIndex(choice, names);
|
|
157
157
|
if (oldName) {
|
|
158
158
|
const server = mcpServers[oldName];
|
|
159
159
|
if (!server) console.log("该服务器配置缺失,将重新录入");
|
|
@@ -173,17 +173,33 @@ const configMcp = async (mcpServers) => {
|
|
|
173
173
|
}
|
|
174
174
|
};
|
|
175
175
|
/**
|
|
176
|
+
* 询问MODEL:能连上服务时先列出GET /models的模型,输入序号即可选中,同时也支持直接手输模型名
|
|
177
|
+
* @param baseUrl 刚录入的BASE_URL
|
|
178
|
+
* @param apiKey 刚录入的APIKEY
|
|
179
|
+
* @param fallback 直接回车时沿用的模型
|
|
180
|
+
*/
|
|
181
|
+
const askModel = async (baseUrl, apiKey, fallback) => {
|
|
182
|
+
const [error, models] = await to(baseUrl ? listModels(baseUrl, apiKey) : Promise.resolve([]));
|
|
183
|
+
const names = models ?? [];
|
|
184
|
+
if (error) console.log(`获取模型列表失败:${extractErrorMessage(error)}`);
|
|
185
|
+
else if (names.length === 0) console.log("服务端未返回任何模型,请手动输入模型名");
|
|
186
|
+
else {
|
|
187
|
+
console.log("可用模型(输入序号快速选择,也可直接输入模型名):");
|
|
188
|
+
names.forEach((name, i) => {
|
|
189
|
+
console.log(` ${i + 1}. ${name}`);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
const input = (await ask(`MODEL [当前为${fallback}]: `)).trim();
|
|
193
|
+
return pickByIndex(input, names) || input || fallback;
|
|
194
|
+
};
|
|
195
|
+
/**
|
|
176
196
|
* setup入口:依次询问BASE_URL / MODEL / APIKEY,确认后写入全局配置
|
|
177
197
|
*/
|
|
178
198
|
async function runSetup() {
|
|
179
199
|
const config = loadConfig();
|
|
180
|
-
if (config) {
|
|
181
|
-
console.log(`当前配置:BASE_URL ${config.baseUrl},APIKEY ${config.apiKey},MODEL ${config.model}`);
|
|
182
|
-
console.log("直接回车可沿用当前值。\n");
|
|
183
|
-
}
|
|
184
200
|
const baseUrl = await ask(`BASE_URL [当前为${config?.baseUrl}]: `) || config?.baseUrl;
|
|
185
201
|
const apiKey = await ask(`APIKEY [当前为${config?.apiKey}]: `) || config?.apiKey;
|
|
186
|
-
const model = await
|
|
202
|
+
const model = await askModel(baseUrl, apiKey, config?.model);
|
|
187
203
|
const mcpServers = { ...config?.mcpServers };
|
|
188
204
|
if ((await ask("是否配置MCP服务器?(y/n): ")).trim().toLowerCase() === "y") await configMcp(mcpServers);
|
|
189
205
|
try {
|
|
@@ -308,6 +324,8 @@ var TUI = class {
|
|
|
308
324
|
md = null;
|
|
309
325
|
/** 是否允许输入 */
|
|
310
326
|
isBusy = false;
|
|
327
|
+
/** 是否正停在提示符上等输入(决定printNotice要不要重绘提示符) */
|
|
328
|
+
isPrompting = false;
|
|
311
329
|
/** 上次打印内容所属的状态(reasoning/content/tool)
|
|
312
330
|
* 用于在新的状态开始时改变样式、打印前缀
|
|
313
331
|
*/
|
|
@@ -319,19 +337,28 @@ var TUI = class {
|
|
|
319
337
|
constructor(onPrompt) {
|
|
320
338
|
this.onPrompt = onPrompt;
|
|
321
339
|
}
|
|
322
|
-
/**
|
|
323
|
-
|
|
340
|
+
/**
|
|
341
|
+
* 启动TUI
|
|
342
|
+
* @param initialPrompt 传入时先替用户发出这一轮(ai “提示词”的快捷方式),之后照常等输入
|
|
343
|
+
*/
|
|
344
|
+
start(initialPrompt) {
|
|
324
345
|
this.rl = readline.createInterface({
|
|
325
346
|
input: process.stdin,
|
|
326
347
|
output: process.stdout
|
|
327
348
|
});
|
|
328
349
|
process.stdin.on("keypress", this.onKeypress);
|
|
329
|
-
this.prompting();
|
|
330
350
|
this.rl.on("close", () => {
|
|
331
351
|
process.stdin.off("keypress", this.onKeypress);
|
|
332
352
|
this.rl?.close();
|
|
333
353
|
this.rl = null;
|
|
334
354
|
});
|
|
355
|
+
if (initialPrompt) {
|
|
356
|
+
process.stdout.write(`> ${initialPrompt}\n`);
|
|
357
|
+
this.submit({
|
|
358
|
+
text: initialPrompt,
|
|
359
|
+
images: []
|
|
360
|
+
});
|
|
361
|
+
} else this.prompting();
|
|
335
362
|
}
|
|
336
363
|
/** 监听用户的特定按键 */
|
|
337
364
|
onKeypress = (_str, key) => {
|
|
@@ -341,7 +368,9 @@ var TUI = class {
|
|
|
341
368
|
/** 监听用户输入 */
|
|
342
369
|
prompting() {
|
|
343
370
|
if (this.isBusy) return;
|
|
371
|
+
this.isPrompting = true;
|
|
344
372
|
this.rl?.question("> ", async (answer) => {
|
|
373
|
+
this.isPrompting = false;
|
|
345
374
|
await this.clipboardTask;
|
|
346
375
|
const text = answer.trim();
|
|
347
376
|
const images = this.pendingImages;
|
|
@@ -350,15 +379,22 @@ var TUI = class {
|
|
|
350
379
|
this.prompting();
|
|
351
380
|
return;
|
|
352
381
|
}
|
|
353
|
-
this.
|
|
354
|
-
await this.onPrompt?.({
|
|
382
|
+
await this.submit({
|
|
355
383
|
text,
|
|
356
384
|
images
|
|
357
385
|
});
|
|
358
|
-
this.isBusy = false;
|
|
359
|
-
this.prompting();
|
|
360
386
|
});
|
|
361
387
|
}
|
|
388
|
+
/**
|
|
389
|
+
* 走完整的一轮:把输入交给调用方,结束后继续等用户输入
|
|
390
|
+
* @remarks 提示词来源可以是用户敲的,也可以是start()传进来的initialPrompt
|
|
391
|
+
*/
|
|
392
|
+
async submit(input) {
|
|
393
|
+
this.isBusy = true;
|
|
394
|
+
await this.onPrompt?.(input);
|
|
395
|
+
this.isBusy = false;
|
|
396
|
+
this.prompting();
|
|
397
|
+
}
|
|
362
398
|
/** 读取剪贴板图片,推入待发送列表 */
|
|
363
399
|
async attachImage() {
|
|
364
400
|
const image = await readClipboardImage();
|
|
@@ -401,6 +437,27 @@ var TUI = class {
|
|
|
401
437
|
if (!process.stdout.isTTY) return text;
|
|
402
438
|
return `\x1b[${ansiCode}m${text}\x1b[0m`;
|
|
403
439
|
}
|
|
440
|
+
/** 把一个Agent事件分发到对应的print方法上 */
|
|
441
|
+
render(event) {
|
|
442
|
+
switch (event.type) {
|
|
443
|
+
case "reasoning_delta":
|
|
444
|
+
this.printReasoning(event.delta);
|
|
445
|
+
break;
|
|
446
|
+
case "content_delta":
|
|
447
|
+
this.printContent(event.delta);
|
|
448
|
+
break;
|
|
449
|
+
case "tool_call":
|
|
450
|
+
this.printToolCall(event.name, event.args);
|
|
451
|
+
break;
|
|
452
|
+
case "tool_result":
|
|
453
|
+
this.printToolResult(event.name, event.result);
|
|
454
|
+
break;
|
|
455
|
+
case "error":
|
|
456
|
+
this.printContent(event.message);
|
|
457
|
+
break;
|
|
458
|
+
case "done": this.printFinish(event.finishReason, event.usage);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
404
461
|
/** 流式打印AI思考内容(灰色) */
|
|
405
462
|
printReasoning(delta) {
|
|
406
463
|
if (this.preparePrint("reasoning_delta")) delta = `[思考内容] ${delta.replaceAll("\n", "")}`;
|
|
@@ -432,10 +489,10 @@ var TUI = class {
|
|
|
432
489
|
this.preparePrint("done");
|
|
433
490
|
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"));
|
|
434
491
|
}
|
|
435
|
-
/**
|
|
492
|
+
/** 打印一行黄字提示,正停在提示符上时顺便重绘输入行 */
|
|
436
493
|
printNotice(message) {
|
|
437
494
|
process.stdout.write(`${this.colorize(message, "93")}\n`);
|
|
438
|
-
this.rl?.prompt(true);
|
|
495
|
+
if (this.isPrompting) this.rl?.prompt(true);
|
|
439
496
|
}
|
|
440
497
|
};
|
|
441
498
|
//#endregion
|
|
@@ -454,8 +511,11 @@ const buildUserContent = (text, images) => {
|
|
|
454
511
|
content.push(...images);
|
|
455
512
|
return content;
|
|
456
513
|
};
|
|
457
|
-
/**
|
|
458
|
-
|
|
514
|
+
/**
|
|
515
|
+
* 启动交互对话:配置来自全局配置文件,环境变量可临时覆盖
|
|
516
|
+
* @param prompt 带上提示词时,先自动发出这一轮,之后照常继续对话
|
|
517
|
+
*/
|
|
518
|
+
const startChat = async (prompt) => {
|
|
459
519
|
const config = loadConfig();
|
|
460
520
|
if (!config) {
|
|
461
521
|
console.error("请先运行 `ai setup` 配置一个模型");
|
|
@@ -483,38 +543,23 @@ const startChat = async () => {
|
|
|
483
543
|
get_time_default,
|
|
484
544
|
...await mcpLoading
|
|
485
545
|
];
|
|
486
|
-
for await (const e of runAgent(model, messages, tools))
|
|
487
|
-
case "reasoning_delta":
|
|
488
|
-
tui.printReasoning(e.delta);
|
|
489
|
-
break;
|
|
490
|
-
case "content_delta":
|
|
491
|
-
tui.printContent(e.delta);
|
|
492
|
-
break;
|
|
493
|
-
case "tool_call":
|
|
494
|
-
tui.printToolCall(e.name, e.args);
|
|
495
|
-
break;
|
|
496
|
-
case "tool_result":
|
|
497
|
-
tui.printToolResult(e.name, e.result);
|
|
498
|
-
break;
|
|
499
|
-
case "error":
|
|
500
|
-
tui.printContent(e.message);
|
|
501
|
-
break;
|
|
502
|
-
case "done": tui.printFinish(e.finishReason, e.usage);
|
|
503
|
-
}
|
|
546
|
+
for await (const e of runAgent(model, messages, tools)) tui.render(e);
|
|
504
547
|
});
|
|
505
|
-
tui.start();
|
|
548
|
+
tui.start(prompt);
|
|
506
549
|
};
|
|
507
550
|
/** 打印命令用法 */
|
|
508
551
|
const printHelp = () => {
|
|
509
|
-
console.log(`用法: ai [
|
|
552
|
+
console.log(`用法: ai [命令/提示词]
|
|
510
553
|
|
|
511
554
|
命令:
|
|
512
555
|
--help 显示帮助
|
|
513
556
|
setup 交互式配置模型APIKEY / BASE_URL / MODEL(保存到 ~/.@nickyzj2023/ai/config.json)
|
|
514
557
|
|
|
515
|
-
|
|
558
|
+
带上提示词时,启动对话的同时先把这段提示词发出去(如\`ai 明天适合洗车吗\`)
|
|
559
|
+
不带参数则直接进入对话`);
|
|
516
560
|
};
|
|
517
|
-
const
|
|
561
|
+
const args = process.argv.slice(2);
|
|
562
|
+
const command = args[0];
|
|
518
563
|
switch (command) {
|
|
519
564
|
case void 0:
|
|
520
565
|
startChat();
|
|
@@ -527,8 +572,11 @@ switch (command) {
|
|
|
527
572
|
printHelp();
|
|
528
573
|
break;
|
|
529
574
|
default:
|
|
530
|
-
|
|
531
|
-
|
|
575
|
+
if (command.startsWith("-")) {
|
|
576
|
+
console.error(`未知命令:${command}(可以运行ai --help查看用法)`);
|
|
577
|
+
process.exit(1);
|
|
578
|
+
}
|
|
579
|
+
startChat(args.join(" ").trim() || void 0);
|
|
532
580
|
}
|
|
533
581
|
//#endregion
|
|
534
582
|
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fetcher } from "@nickyzj2023/utils";
|
|
1
2
|
//#region src/utils/helper.ts
|
|
2
3
|
/**
|
|
3
4
|
* 辅助定义一个POST /chat/completions支持的model参数
|
|
@@ -44,6 +45,17 @@ const estimateTextTokens = (text) => {
|
|
|
44
45
|
return Math.ceil(words * 1.5 + others / 4);
|
|
45
46
|
};
|
|
46
47
|
/**
|
|
48
|
+
* 列出GET /models返回的模型id
|
|
49
|
+
* @param baseUrl 接口前缀,如http://127.0.0.1:11434/v1
|
|
50
|
+
* @param apiKey 本地llama.cpp等不校验鉴权的服务可以不传,不传就不带Authorization头
|
|
51
|
+
* @returns 模型的id列表
|
|
52
|
+
* @remarks 请求失败(服务没起、鉴权不过等)会抛异常,由调用方自行兜底
|
|
53
|
+
*/
|
|
54
|
+
const listModels = async (baseUrl, apiKey) => {
|
|
55
|
+
const { data } = await fetcher(baseUrl, { headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : void 0 }).get("/models");
|
|
56
|
+
return (data ?? []).map((model) => model.id).filter((id) => typeof id === "string");
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
47
59
|
* 根据上下文里的中/英文/多模态消息,估算出可能消耗的token
|
|
48
60
|
* - 单词 ≈ 1.5token
|
|
49
61
|
* - 标点/空白等非词字符每4个 ≈ 1token
|
|
@@ -62,4 +74,4 @@ const estimateTokens = (messages) => {
|
|
|
62
74
|
}, 0);
|
|
63
75
|
};
|
|
64
76
|
//#endregion
|
|
65
|
-
export { estimateTokens as i, defineTool as n, estimateTextTokens as r, defineModel as t };
|
|
77
|
+
export { listModels as a, estimateTokens as i, defineTool as n, estimateTextTokens as r, defineModel as t };
|
package/dist/index.d.mts
CHANGED
|
@@ -137,6 +137,14 @@ declare const defineModel: (config: Model) => Model;
|
|
|
137
137
|
*/
|
|
138
138
|
declare const defineTool: (name: ToolDefinition["function"]["name"], description: ToolDefinition["function"]["description"], properties: ToolDefinition["function"]["parameters"]["properties"], execute: ToolDefinition["execute"]) => ToolDefinition;
|
|
139
139
|
declare const estimateTextTokens: (text: string) => number;
|
|
140
|
+
/**
|
|
141
|
+
* 列出GET /models返回的模型id
|
|
142
|
+
* @param baseUrl 接口前缀,如http://127.0.0.1:11434/v1
|
|
143
|
+
* @param apiKey 本地llama.cpp等不校验鉴权的服务可以不传,不传就不带Authorization头
|
|
144
|
+
* @returns 模型的id列表
|
|
145
|
+
* @remarks 请求失败(服务没起、鉴权不过等)会抛异常,由调用方自行兜底
|
|
146
|
+
*/
|
|
147
|
+
declare const listModels: (baseUrl: string, apiKey?: string) => Promise<string[]>;
|
|
140
148
|
/**
|
|
141
149
|
* 根据上下文里的中/英文/多模态消息,估算出可能消耗的token
|
|
142
150
|
* - 单词 ≈ 1.5token
|
|
@@ -145,4 +153,4 @@ declare const estimateTextTokens: (text: string) => number;
|
|
|
145
153
|
*/
|
|
146
154
|
declare const estimateTokens: (messages?: Message[]) => number;
|
|
147
155
|
//#endregion
|
|
148
|
-
export { type AgentEvent, type AudioContent, type ChatCompletionsChunk, type ContentPart, type FinishReason, type ImageContent, type LLMEvent, type Message, type Modality, type Model, type TextContent, type ToolCall, type ToolDefinition, type Usage, type VideoContent, compact, defineModel, defineTool, estimateTextTokens, estimateTokens, _default as getTime, _default$1 as getWeather, runAgent, stream };
|
|
156
|
+
export { type AgentEvent, type AudioContent, type ChatCompletionsChunk, type ContentPart, type FinishReason, type ImageContent, type LLMEvent, type Message, type Modality, type Model, type TextContent, type ToolCall, type ToolDefinition, type Usage, type VideoContent, compact, defineModel, defineTool, estimateTextTokens, estimateTokens, _default as getTime, _default$1 as getWeather, listModels, runAgent, stream };
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as stream, i as runAgent, n as get_weather_default, r as get_time_default, t as compact } from "./src-
|
|
2
|
-
import { i as estimateTokens, n as defineTool, r as estimateTextTokens, t as defineModel } from "./helper-
|
|
3
|
-
export { compact, defineModel, defineTool, estimateTextTokens, estimateTokens, get_time_default as getTime, get_weather_default as getWeather, runAgent, stream };
|
|
1
|
+
import { a as stream, i as runAgent, n as get_weather_default, r as get_time_default, t as compact } from "./src-DPR9YjB3.mjs";
|
|
2
|
+
import { a as listModels, i as estimateTokens, n as defineTool, r as estimateTextTokens, t as defineModel } from "./helper-DJw58Gmz.mjs";
|
|
3
|
+
export { compact, defineModel, defineTool, estimateTextTokens, estimateTokens, get_time_default as getTime, get_weather_default as getWeather, listModels, runAgent, stream };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as estimateTokens, n as defineTool, r as estimateTextTokens } from "./helper-
|
|
1
|
+
import { i as estimateTokens, n as defineTool, r as estimateTextTokens } from "./helper-DJw58Gmz.mjs";
|
|
2
2
|
import { createXMLText, extractErrorMessage, fetcher, logger, parseSSE, pick, to } from "@nickyzj2023/utils";
|
|
3
3
|
//#region src/llm.ts
|
|
4
4
|
/**
|
|
@@ -308,7 +308,7 @@ const summarizeMessages = async (compressible, options) => {
|
|
|
308
308
|
logger("消息太少,无需总结");
|
|
309
309
|
return 0;
|
|
310
310
|
}
|
|
311
|
-
beforeSummarize?.(summarizable);
|
|
311
|
+
await beforeSummarize?.(summarizable);
|
|
312
312
|
const count = summarizable.length;
|
|
313
313
|
summarizable.push({
|
|
314
314
|
role: "system",
|
package/dist/tools/mcp.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as loadMCPTools, t as MCPRouter } from "../mcp-
|
|
1
|
+
import { n as loadMCPTools, t as MCPRouter } from "../mcp-BBjcb8es.mjs";
|
|
2
2
|
export { MCPRouter, loadMCPTools };
|