@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 CHANGED
@@ -71,6 +71,9 @@ ai setup
71
71
 
72
72
  # 启动对话
73
73
  ai
74
+
75
+ # 直接开聊,本质是“ai->输入提示词->回车”的语法糖
76
+ ai 你好
74
77
  ```
75
78
 
76
79
  配置保存在 `~/.@nickyzj2023/ai/config.json`,任意目录下执行`ai`都能读取到
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-CKKPKPMG.mjs";
2
- import { t as defineModel } from "./helper-juLVGxlQ.mjs";
3
- import { n as loadMCPTools } from "./mcp-DcntQg-a.mjs";
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 pickServerName = (input, names) => {
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 = pickServerName(target, names);
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 = pickServerName(choice, names);
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 ask(`MODEL [当前为${config?.model}]: `) || config?.model;
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
- /** 启动TUI */
323
- start() {
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.isBusy = true;
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
- const startChat = async () => {
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)) switch (e.type) {
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 command = process.argv[2];
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
- console.error(`未知命令:${command}(可以运行ai --help查看用法)`);
531
- process.exit(1);
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-CKKPKPMG.mjs";
2
- import { i as estimateTokens, n as defineTool, r as estimateTextTokens, t as defineModel } from "./helper-juLVGxlQ.mjs";
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 { n as defineTool } from "./helper-juLVGxlQ.mjs";
1
+ import { n as defineTool } from "./helper-DJw58Gmz.mjs";
2
2
  import { extractErrorMessage, isObject, logger, omit } from "@nickyzj2023/utils";
3
3
  //#region src/tools/mcp.ts
4
4
  /** 全局单例MCP加载器 */
@@ -1,4 +1,4 @@
1
- import { i as estimateTokens, n as defineTool, r as estimateTextTokens } from "./helper-juLVGxlQ.mjs";
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",
@@ -1,2 +1,2 @@
1
- import { n as loadMCPTools, t as MCPRouter } from "../mcp-DcntQg-a.mjs";
1
+ import { n as loadMCPTools, t as MCPRouter } from "../mcp-BBjcb8es.mjs";
2
2
  export { MCPRouter, loadMCPTools };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nickyzj2023/ai",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "我的“pi”,参考了pi-from-scratch",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",