@nickyzj2023/ai 1.0.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/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 nickyzj2023
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # ai
2
+
3
+ 男生自用MyPi Coding Agent,0外部依赖
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ # npm
9
+ npm install @nickyzj2023/ai
10
+
11
+ # yarn
12
+ yarn add @nickyzj2023/ai
13
+
14
+ # pnpm
15
+ pnpm add @nickyzj2023/ai
16
+ ```
17
+
18
+ ## 使用方式
19
+
20
+ ### 在项目里使用
21
+
22
+ ```typescript
23
+ import { defineModel, runAgent, getWeather } from "@nickyzj2023/ai";
24
+
25
+ const model = defineModel({
26
+ baseUrl: "https://api.deepseek.com/v1",
27
+ apiKey: process.env.APIKEY,
28
+ model: "deepseek-v4-flash",
29
+ });
30
+
31
+ const tools = [
32
+ // 内置工具
33
+ getWeather,
34
+ getTime,
35
+ // 自定义工具
36
+ defineTool("getRandomNumber", "生成随机数字", {min: {type: "number", description: "最小区间"}, max: {type:"number", description:"最大区间"}}, () => 33550336),
37
+ ];
38
+
39
+ const messages = [{ role: "user", content: "随机一个负无穷到正无穷的整数" }];
40
+
41
+ for await (const e of runAgent(model, messages, tools)) {
42
+ console.log(e);
43
+ }
44
+ ```
45
+
46
+ ### 在终端里使用
47
+
48
+ ```bash
49
+ # 配置deepseek环境变量、启动对话
50
+ SET APIKEY=sk-xxx && ai
51
+ ```
52
+
53
+ ## License
54
+
55
+ ISC
package/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ export {}
package/dist/cli.mjs ADDED
@@ -0,0 +1,67 @@
1
+ import { a as runAgent, i as defineTool, n as get_weather_default, r as defineModel, t as TUI } from "./tui-Dz0id9sE.mjs";
2
+ import { fetcher } from "@nickyzj2023/utils";
3
+ import { existsSync } from "node:fs";
4
+ import { loadEnvFile } from "node:process";
5
+ //#region src/tools/get-time.ts
6
+ const weekdayNames = {
7
+ Sunday: "星期日",
8
+ Monday: "星期一",
9
+ Tuesday: "星期二",
10
+ Wednesday: "星期三",
11
+ Thursday: "星期四",
12
+ Friday: "星期五",
13
+ Saturday: "星期六"
14
+ };
15
+ var get_time_default = defineTool("get_time", "查询指定时区的当前时间", { timezone: {
16
+ type: "string",
17
+ description: "完整的IANA时区名称,如Europe/Amsterdam。用户未明确提及时,传入对方语言对应的时区,例如对方使用中文时可传入Asia/Shanghai。",
18
+ required: true
19
+ } }, async ({ timezone }) => {
20
+ const data = await fetcher("https://timeapi.io/api", { params: { timezone } }).get("/time/current/zone");
21
+ const pad = (n) => String(n).padStart(2, "0");
22
+ return [
23
+ `时区:${data.timeZone}${data.dstActive ? "(夏令时已生效)" : ""}`,
24
+ `日期:${data.year}-${pad(data.month)}-${pad(data.day)}(${weekdayNames[data.dayOfWeek] ?? data.dayOfWeek})`,
25
+ `时间:${pad(data.hour)}:${pad(data.minute)}:${pad(data.seconds)}`
26
+ ].join("\n");
27
+ });
28
+ //#endregion
29
+ //#region src/cli.ts
30
+ if (existsSync(".env")) loadEnvFile(".env");
31
+ const model = defineModel({
32
+ baseUrl: process.env.BASE_URL ?? "https://api.deepseek.com/v1",
33
+ apiKey: process.env.APIKEY,
34
+ model: process.env.MODEL ?? "deepseek-v4-flash"
35
+ });
36
+ if (!model.apiKey) {
37
+ console.error("请先在环境变量或.env文件中填入APIKEY(目前仅支持DeepSeek)");
38
+ process.exit(1);
39
+ }
40
+ const messages = [];
41
+ const tools = [get_weather_default, get_time_default];
42
+ const tui = new TUI(async (input) => {
43
+ messages.push({
44
+ role: "user",
45
+ content: input
46
+ });
47
+ for await (const e of runAgent(model, messages, tools)) switch (e.type) {
48
+ case "reasoning_delta":
49
+ tui.printReasoning(e.delta);
50
+ break;
51
+ case "content_delta":
52
+ tui.printContent(e.delta);
53
+ break;
54
+ case "tool_call":
55
+ tui.printToolCall(e.name, e.args);
56
+ break;
57
+ case "tool_result":
58
+ tui.printToolResult(e.name, e.result);
59
+ break;
60
+ case "done": tui.printFinish(e.finishReason, e.usage);
61
+ }
62
+ });
63
+ tui.start();
64
+ //#endregion
65
+ export {};
66
+
67
+ //# sourceMappingURL=cli.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.mjs","names":["getWeather","getTime"],"sources":["../src/tools/get-time.ts","../src/cli.ts"],"sourcesContent":["import { fetcher } from \"@nickyzj2023/utils\";\nimport { defineTool } from \"../helper.js\";\n\n// 英文星期名映射为中文,让输出直接可读;未知值时回退到原始英文\nconst weekdayNames: Record<string, string> = {\n\tSunday: \"星期日\",\n\tMonday: \"星期一\",\n\tTuesday: \"星期二\",\n\tWednesday: \"星期三\",\n\tThursday: \"星期四\",\n\tFriday: \"星期五\",\n\tSaturday: \"星期六\",\n};\n\nexport default defineTool(\n\t\"get_time\",\n\t\"查询指定时区的当前时间\",\n\t{\n\t\ttimezone: {\n\t\t\ttype: \"string\",\n\t\t\tdescription:\n\t\t\t\t\"完整的IANA时区名称,如Europe/Amsterdam。用户未明确提及时,传入对方语言对应的时区,例如对方使用中文时可传入Asia/Shanghai。\",\n\t\t\trequired: true,\n\t\t},\n\t},\n\tasync ({ timezone }) => {\n\t\tconst api = fetcher(\"https://timeapi.io/api\", {\n\t\t\tparams: {\n\t\t\t\ttimezone,\n\t\t\t},\n\t\t});\n\t\tconst data = await api.get<any>(\"/time/current/zone\");\n\t\t// 时/分/秒统一补零成两位,保证输出时间文本格式一致\n\t\tconst pad = (n: number) => String(n).padStart(2, \"0\");\n\t\treturn [\n\t\t\t`时区:${data.timeZone}${data.dstActive ? \"(夏令时已生效)\" : \"\"}`,\n\t\t\t`日期:${data.year}-${pad(data.month)}-${pad(data.day)}(${weekdayNames[data.dayOfWeek] ?? data.dayOfWeek})`,\n\t\t\t`时间:${pad(data.hour)}:${pad(data.minute)}:${pad(data.seconds)}`,\n\t\t].join(\"\\n\");\n\t},\n);\n","import { existsSync } from \"node:fs\";\nimport { loadEnvFile } from \"node:process\";\nimport { runAgent } from \"./agent.js\";\nimport { defineModel } from \"./helper.js\";\nimport getTime from \"./tools/get-time.js\";\nimport getWeather from \"./tools/get-weather.js\";\nimport { TUI } from \"./tui.js\";\nimport type { Message } from \"./types.js\";\n\n// 仅在存在.env时加载:发布后用户可能没有该文件,直接调用会抛错\nif (existsSync(\".env\")) {\n\tloadEnvFile(\".env\");\n}\n\n// 1. 读取配置(环境变量可覆盖默认的DeepSeek地址与模型)\nconst model = defineModel({\n\tbaseUrl: process.env.BASE_URL ?? \"https://api.deepseek.com/v1\",\n\tapiKey: process.env.APIKEY,\n\tmodel: process.env.MODEL ?? \"deepseek-v4-flash\",\n});\n\nif (!model.apiKey) {\n\tconsole.error(\"请先在环境变量或.env文件中填入APIKEY(目前仅支持DeepSeek)\");\n\tprocess.exit(1);\n}\n\n// 2. 读取上下文\nconst messages: Message[] = [];\n\n// 3. 读取工具\nconst tools = [getWeather, getTime];\n\n// 4. 启动TUI,监听用户输入,按下回车后调用Agent\nconst tui = new TUI(async (input) => {\n\tmessages.push({ role: \"user\", content: input });\n\n\tfor await (const e of runAgent(model, messages, tools)) {\n\t\tswitch (e.type) {\n\t\t\tcase \"reasoning_delta\": {\n\t\t\t\ttui.printReasoning(e.delta);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"content_delta\": {\n\t\t\t\ttui.printContent(e.delta);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"tool_call\": {\n\t\t\t\ttui.printToolCall(e.name, e.args);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"tool_result\": {\n\t\t\t\ttui.printToolResult(e.name, e.result);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"done\": {\n\t\t\t\ttui.printFinish(e.finishReason, e.usage);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n});\ntui.start();\n"],"mappings":";;;;;AAIA,MAAM,eAAuC;CAC5C,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,WAAW;CACX,UAAU;CACV,QAAQ;CACR,UAAU;AACX;AAEA,IAAA,mBAAe,WACd,YACA,eACA,EACC,UAAU;CACT,MAAM;CACN,aACC;CACD,UAAU;AACX,EACD,GACA,OAAO,EAAE,eAAe;CAMvB,MAAM,OAAO,MALD,QAAQ,0BAA0B,EAC7C,QAAQ,EACP,SACD,EACD,CACqB,CAAC,CAAC,IAAS,oBAAoB;CAEpD,MAAM,OAAO,MAAc,OAAO,CAAC,CAAC,CAAC,SAAS,GAAG,GAAG;CACpD,OAAO;EACN,MAAM,KAAK,WAAW,KAAK,YAAY,aAAa;EACpD,MAAM,KAAK,KAAK,GAAG,IAAI,KAAK,KAAK,EAAE,GAAG,IAAI,KAAK,GAAG,EAAE,GAAG,aAAa,KAAK,cAAc,KAAK,UAAU;EACtG,MAAM,IAAI,KAAK,IAAI,EAAE,GAAG,IAAI,KAAK,MAAM,EAAE,GAAG,IAAI,KAAK,OAAO;CAC7D,CAAC,CAAC,KAAK,IAAI;AACZ,CACD;;;AC9BA,IAAI,WAAW,MAAM,GACpB,YAAY,MAAM;AAInB,MAAM,QAAQ,YAAY;CACzB,SAAS,QAAQ,IAAI,YAAY;CACjC,QAAQ,QAAQ,IAAI;CACpB,OAAO,QAAQ,IAAI,SAAS;AAC7B,CAAC;AAED,IAAI,CAAC,MAAM,QAAQ;CAClB,QAAQ,MAAM,wCAAwC;CACtD,QAAQ,KAAK,CAAC;AACf;AAGA,MAAM,WAAsB,CAAC;AAG7B,MAAM,QAAQ,CAACA,qBAAYC,gBAAO;AAGlC,MAAM,MAAM,IAAI,IAAI,OAAO,UAAU;CACpC,SAAS,KAAK;EAAE,MAAM;EAAQ,SAAS;CAAM,CAAC;CAE9C,WAAW,MAAM,KAAK,SAAS,OAAO,UAAU,KAAK,GACpD,QAAQ,EAAE,MAAV;EACC,KAAK;GACJ,IAAI,eAAe,EAAE,KAAK;GAC1B;EAED,KAAK;GACJ,IAAI,aAAa,EAAE,KAAK;GACxB;EAED,KAAK;GACJ,IAAI,cAAc,EAAE,MAAM,EAAE,IAAI;GAChC;EAED,KAAK;GACJ,IAAI,gBAAgB,EAAE,MAAM,EAAE,MAAM;GACpC;EAED,KAAK,QACJ,IAAI,YAAY,EAAE,cAAc,EAAE,KAAK;CAGzC;AAEF,CAAC;AACD,IAAI,MAAM"}
@@ -0,0 +1,210 @@
1
+ //#region src/types.d.ts
2
+ type Model = {
3
+ baseUrl: string;
4
+ /** 如果不传,会尝试使用GET {baseUrl}/models获取到的第一个模型 */
5
+ model?: string;
6
+ /** 使用本地llama.cpp等服务时可以不填 */
7
+ apiKey?: string;
8
+ /**
9
+ * 模型支持的消息输入类型
10
+ * @default ["text"]
11
+ * @remarks 会在每轮对话前校验消息类型,存在不支持的输入时报错
12
+ */
13
+ modalities?: Modality[];
14
+ /**
15
+ * 模型的最大上下文
16
+ * @default 131072
17
+ * @remarks 会在每轮对话结束后检查上下文阈值,自动压缩消息
18
+ */
19
+ context?: number;
20
+ /**
21
+ * 自定义请求体,会在每轮请求时带上
22
+ * @example chat_template_kwargs: { enable_thinking: false }
23
+ */
24
+ [key: string]: unknown;
25
+ };
26
+ /** 文/图/音/视频,暂未实现文件输入 */
27
+ type Modality = "text" | "image" | "audio" | "video";
28
+ type Message = {
29
+ role: "system" | "user" | "assistant" | "tool";
30
+ reasoning?: string | null;
31
+ content: string | ContentPart[];
32
+ tool_calls?: ToolCall[];
33
+ tool_call_id?: string;
34
+ [key: string]: unknown;
35
+ };
36
+ type ContentPart = TextContent | ImageContent | AudioContent | VideoContent;
37
+ type TextContent = {
38
+ type: "text";
39
+ text: string;
40
+ };
41
+ type ImageContent = {
42
+ type: "image_url";
43
+ image_url: {
44
+ url: string;
45
+ };
46
+ };
47
+ type AudioContent = {
48
+ type: "input_audio";
49
+ input_audio: {
50
+ /** 使用公网可访问的音频链接 */
51
+ url?: string;
52
+ /** 使用base64 */
53
+ data?: string;
54
+ format: string;
55
+ };
56
+ };
57
+ type VideoContent = {
58
+ type: "video_url";
59
+ video_url: {
60
+ url: string;
61
+ };
62
+ };
63
+ type ToolDefinition = {
64
+ type: "function";
65
+ function: {
66
+ name: string;
67
+ description: string;
68
+ parameters: {
69
+ type: "object";
70
+ properties: Record<string, {
71
+ type: string;
72
+ description?: string;
73
+ /** defineTool()提供的语法糖,等价于在parameters.required.push(当前property key) */
74
+ required?: boolean;
75
+ }>;
76
+ required?: string[];
77
+ };
78
+ };
79
+ /** 工具的实际执行函数 */
80
+ execute: (...args: any) => any;
81
+ };
82
+ type ToolCall = {
83
+ id: string;
84
+ type: "function";
85
+ function: {
86
+ name: string;
87
+ arguments: string;
88
+ };
89
+ };
90
+ /** stream()对外输出的事件 */
91
+ type LLMEvent = {
92
+ type: "reasoning_delta";
93
+ delta: string;
94
+ } | {
95
+ type: "content_delta";
96
+ delta: string;
97
+ } | {
98
+ type: "tool_call";
99
+ id: string;
100
+ name: string;
101
+ args: any;
102
+ } | {
103
+ type: "done";
104
+ finishReason: FinishReason;
105
+ usage?: Usage;
106
+ } | {
107
+ type: "error";
108
+ message: string;
109
+ };
110
+ /** stream()内部要处理的事件流 */
111
+ type ChatCompletionsChunk = {
112
+ id: string;
113
+ object: "chat.completion.chunk";
114
+ created: number;
115
+ model: string;
116
+ choices: {
117
+ index: number;
118
+ delta: Pick<Message, "reasoning" | "content"> & {
119
+ tool_calls?: ({
120
+ index: number;
121
+ } & Partial<ToolCall>)[];
122
+ };
123
+ finish_reason: FinishReason;
124
+ }[];
125
+ usage?: Usage;
126
+ } | "[DONE]";
127
+ /** 模型可能返回的finish_reason值:停止/工具调用/上下文超限 */
128
+ type FinishReason = "stop" | "tool_calls" | "length" | null;
129
+ type Usage = {
130
+ prompt_tokens: number;
131
+ completion_tokens: number;
132
+ total_tokens: number;
133
+ [key: string]: any;
134
+ };
135
+ type AgentEvent = LLMEvent | {
136
+ type: "tool_result";
137
+ id: string;
138
+ name: string;
139
+ result: string;
140
+ };
141
+ //#endregion
142
+ //#region src/agent.d.ts
143
+ declare function runAgent(model: Model, messages: Message[], tools: ToolDefinition[]): AsyncGenerator<AgentEvent>;
144
+ //#endregion
145
+ //#region src/helper.d.ts
146
+ /**
147
+ * 辅助定义一个POST /chat/completions支持的model参数
148
+ * @remarks 只有baseUrl字段是必须的
149
+ */
150
+ declare const defineModel: (config: Model) => Model;
151
+ /**
152
+ * 辅助定义一个POST /chat/completions支持的tool对象
153
+ * @param execute 实际执行工具的函数
154
+ */
155
+ declare const defineTool: (name: ToolDefinition["function"]["name"], description: ToolDefinition["function"]["description"], properties: ToolDefinition["function"]["parameters"]["properties"], execute: ToolDefinition["execute"]) => ToolDefinition;
156
+ //#endregion
157
+ //#region src/llm.d.ts
158
+ /**
159
+ * 流式请求模型,转发
160
+ */
161
+ declare function stream(model: Model, messages: Message[], tools?: ToolDefinition[]): AsyncGenerator<LLMEvent>;
162
+ //#endregion
163
+ //#region src/tools/get-weather.d.ts
164
+ declare const _default: ToolDefinition;
165
+ //#endregion
166
+ //#region src/tui.d.ts
167
+ declare class TUI {
168
+ private rl;
169
+ private onPrompt;
170
+ /** 是否允许输入 */
171
+ private isBusy;
172
+ /** 上次打印内容所属的状态(reasoning/content/tool)
173
+ * 用于在新的状态开始时改变样式、打印前缀
174
+ */
175
+ private prevPrintType;
176
+ /**
177
+ * 实例化TUI时,接收一个“发出用户提示词”的回调函数
178
+ * UI只做UI的事,提示词发给谁让调用方决定
179
+ */
180
+ constructor(onPrompt: (prompt: string) => void | Promise<void>);
181
+ /** 启动TUI */
182
+ start(): void;
183
+ /** 监听用户输入 */
184
+ private prompting;
185
+ /**
186
+ * 所有print方法调用前的统一入口(相当于“父类逻辑”):
187
+ * 状态切换时打印换行,并记录当前状态。
188
+ * @returns 是否发生了状态切换
189
+ */
190
+ private preparePrint;
191
+ /**
192
+ * 为文本添加ANSI颜色;非TTY输出(重定向/管道)时返回原文本,避免日志出现转义码。
193
+ * @param text 原始文本
194
+ * @param ansiCode ANSI颜色代码,如 "90"(亮黑,大多数终端显示为灰色)
195
+ */
196
+ private colorize;
197
+ /** 流式打印AI思考内容(灰色) */
198
+ printReasoning(delta: string): void;
199
+ /** 流式打印AI回复内容 */
200
+ printContent(delta: string): void;
201
+ /** 打印工具调用 */
202
+ printToolCall(name: string, args: any): void;
203
+ /** 打印工具结果 */
204
+ printToolResult(name: string, result: string): void;
205
+ /** 打印轮次结束原因、token消耗 */
206
+ printFinish(finishReason: FinishReason, usage?: Usage): void;
207
+ }
208
+ //#endregion
209
+ export { type AgentEvent, type AudioContent, type ChatCompletionsChunk, type ContentPart, type FinishReason, type ImageContent, type LLMEvent, type Message, type Modality, type Model, TUI, type TextContent, type ToolCall, type ToolDefinition, type Usage, type VideoContent, defineModel, defineTool, _default as getWeather, runAgent, stream };
210
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { a as runAgent, i as defineTool, n as get_weather_default, o as stream, r as defineModel, t as TUI } from "./tui-Dz0id9sE.mjs";
2
+ export { TUI, defineModel, defineTool, get_weather_default as getWeather, runAgent, stream };
@@ -0,0 +1,300 @@
1
+ import { fetcher, parseSSE, pick, to } from "@nickyzj2023/utils";
2
+ import readline from "node:readline";
3
+ //#region src/llm.ts
4
+ /**
5
+ * 分离ToolDefinition中的valid/invalid字段,前者可以传给模型,后者用于本地运算
6
+ * @returns [validObj, invalidObj]
7
+ */
8
+ const detachToolArguments = (toolDefinition) => {
9
+ return [pick(toolDefinition, ["type", "function"]), pick(toolDefinition, ["execute"])];
10
+ };
11
+ /**
12
+ * 从消息中提取模型的思考内容。
13
+ * 不同供应商,即使都用OpenAI API Compatible接口,输出的思考内容字段也可能不同,例如:
14
+ * - OpenRouter里的思考字段为reasoning
15
+ * - 火山引擎的叫reasoning_content
16
+ * @returns 统一返回reasoning作为思考内容字段
17
+ */
18
+ const extractReasoning = (msgLike) => {
19
+ return msgLike.reasoning || msgLike.reasoning_content;
20
+ };
21
+ /**
22
+ * 流式请求模型,转发
23
+ */
24
+ async function* stream(model, messages, tools = []) {
25
+ const validTools = tools.map((tool) => detachToolArguments(tool)[0]);
26
+ const api = fetcher(model.baseUrl, {
27
+ headers: { Authorization: `Bearer ${model.apiKey}` },
28
+ parser: async (res) => res
29
+ });
30
+ const [error, response] = await to(api.post("/chat/completions", {
31
+ stream: true,
32
+ model: model.model,
33
+ messages,
34
+ tools: validTools
35
+ }));
36
+ if (error) {
37
+ yield {
38
+ type: "error",
39
+ message: error.message
40
+ };
41
+ return;
42
+ }
43
+ const toolCallBuffers = /* @__PURE__ */ new Map();
44
+ let usage;
45
+ let finishReason = null;
46
+ for await (const chunk of parseSSE(response)) {
47
+ if (typeof chunk === "string") continue;
48
+ if (chunk.usage) usage = chunk.usage;
49
+ const choice = chunk.choices?.[0];
50
+ if (!choice) continue;
51
+ const { delta } = choice;
52
+ const { content: contentDelta, tool_calls: toolCalls } = delta;
53
+ const reasoning = extractReasoning(delta);
54
+ if (reasoning) yield {
55
+ type: "reasoning_delta",
56
+ delta: reasoning
57
+ };
58
+ if (contentDelta) yield {
59
+ type: "content_delta",
60
+ delta: contentDelta.toString()
61
+ };
62
+ if (toolCalls) for (const call of toolCalls) {
63
+ const { index = 0, type = "function", id, function: fn, ...extra } = call;
64
+ const existing = toolCallBuffers.getOrInsert(index, {
65
+ id: "",
66
+ type,
67
+ function: {
68
+ name: "",
69
+ arguments: ""
70
+ }
71
+ });
72
+ if (id) existing.id = id;
73
+ if (fn?.name) existing.function.name += fn.name;
74
+ if (fn?.arguments) existing.function.arguments += fn.arguments;
75
+ if (extra) Object.assign(existing, extra);
76
+ toolCallBuffers.set(index, existing);
77
+ }
78
+ if (choice.finish_reason) finishReason = choice.finish_reason;
79
+ }
80
+ for (const [, call] of toolCallBuffers) yield {
81
+ type: "tool_call",
82
+ id: call.id,
83
+ name: call.function.name,
84
+ args: call.function.arguments
85
+ };
86
+ yield {
87
+ type: "done",
88
+ finishReason,
89
+ usage
90
+ };
91
+ }
92
+ //#endregion
93
+ //#region src/agent.ts
94
+ async function* runAgent(model, messages, tools) {
95
+ const toolMap = new Map(tools.map((tool) => [tool.function.name, tool]));
96
+ while (true) {
97
+ let content = "";
98
+ const toolCalls = [];
99
+ for await (const e of stream(model, messages, tools)) switch (e.type) {
100
+ case "reasoning_delta":
101
+ yield e;
102
+ break;
103
+ case "content_delta":
104
+ content += e.delta;
105
+ yield e;
106
+ break;
107
+ case "tool_call":
108
+ toolCalls.push({
109
+ id: e.id,
110
+ type: "function",
111
+ function: {
112
+ name: e.name,
113
+ arguments: e.args
114
+ }
115
+ });
116
+ yield e;
117
+ break;
118
+ case "done": yield e;
119
+ }
120
+ const message = {
121
+ role: "assistant",
122
+ content
123
+ };
124
+ if (toolCalls.length > 0) message.tool_calls = toolCalls;
125
+ messages.push(message);
126
+ if (toolCalls.length === 0) return;
127
+ for (const call of toolCalls) {
128
+ const { name, arguments: args } = call.function;
129
+ const tool = toolMap.get(name);
130
+ let result = "";
131
+ if (!tool) result = `不存在工具“${name}”`;
132
+ else {
133
+ const [error, response] = await to(tool.execute(JSON.parse(args)));
134
+ result = error ? `工具“${name}”执行出错:${error.message}` : String(response);
135
+ }
136
+ messages.push({
137
+ role: "tool",
138
+ tool_call_id: call.id,
139
+ content: result
140
+ });
141
+ yield {
142
+ type: "tool_result",
143
+ id: call.id,
144
+ name,
145
+ result
146
+ };
147
+ }
148
+ }
149
+ }
150
+ //#endregion
151
+ //#region src/helper.ts
152
+ /**
153
+ * 辅助定义一个POST /chat/completions支持的model参数
154
+ * @remarks 只有baseUrl字段是必须的
155
+ */
156
+ const defineModel = (config) => ({
157
+ modalities: ["text"],
158
+ context: 131072,
159
+ ...config
160
+ });
161
+ /**
162
+ * 辅助定义一个POST /chat/completions支持的tool对象
163
+ * @param execute 实际执行工具的函数
164
+ */
165
+ const defineTool = (name, description, properties, execute) => {
166
+ const _required = [];
167
+ return {
168
+ type: "function",
169
+ function: {
170
+ name,
171
+ description,
172
+ parameters: {
173
+ type: "object",
174
+ properties: Object.entries(properties).reduce((result, [key, property]) => {
175
+ if ("required" in property) {
176
+ _required.push(key);
177
+ delete property.required;
178
+ }
179
+ result[key] = property;
180
+ return result;
181
+ }, {}),
182
+ required: _required
183
+ }
184
+ },
185
+ execute
186
+ };
187
+ };
188
+ //#endregion
189
+ //#region src/tools/get-weather.ts
190
+ var get_weather_default = defineTool("get_weather", "查询指定城市的天气情况", { city: {
191
+ type: "string",
192
+ description: "城市名,如shanghai、tokyo",
193
+ required: true
194
+ } }, async ({ city }) => {
195
+ const data = await fetcher("https://wttr.in", { params: { format: "j1" } }).get(`/${city}`);
196
+ const nearestArea = data.nearest_area[0];
197
+ const currentCondition = data.current_condition[0];
198
+ return [
199
+ `位置:${nearestArea.country[0].value}-${nearestArea.region[0].value}-${nearestArea.areaName[0].value}`,
200
+ `当前温度: ${currentCondition.temp_C}°C`,
201
+ `体感温度: ${currentCondition.FeelsLikeC}°C`,
202
+ `天气状况: ${currentCondition.weatherDesc[0].value}`,
203
+ `湿度: ${currentCondition.humidity}%`
204
+ ].join("\n");
205
+ });
206
+ //#endregion
207
+ //#region src/tui.ts
208
+ var TUI = class {
209
+ rl = null;
210
+ onPrompt = null;
211
+ /** 是否允许输入 */
212
+ isBusy = false;
213
+ /** 上次打印内容所属的状态(reasoning/content/tool)
214
+ * 用于在新的状态开始时改变样式、打印前缀
215
+ */
216
+ prevPrintType = void 0;
217
+ /**
218
+ * 实例化TUI时,接收一个“发出用户提示词”的回调函数
219
+ * UI只做UI的事,提示词发给谁让调用方决定
220
+ */
221
+ constructor(onPrompt) {
222
+ this.onPrompt = onPrompt;
223
+ }
224
+ /** 启动TUI */
225
+ start() {
226
+ this.rl = readline.createInterface({
227
+ input: process.stdin,
228
+ output: process.stdout
229
+ });
230
+ this.prompting();
231
+ this.rl.on("close", () => {
232
+ this.rl?.close();
233
+ this.rl = null;
234
+ });
235
+ }
236
+ /** 监听用户输入 */
237
+ prompting() {
238
+ if (this.isBusy) return;
239
+ this.rl?.question("> ", async (answer) => {
240
+ const input = answer.trim();
241
+ if (!input) {
242
+ this.prompting();
243
+ return;
244
+ }
245
+ this.isBusy = true;
246
+ await this.onPrompt?.(input);
247
+ this.isBusy = false;
248
+ this.prompting();
249
+ });
250
+ }
251
+ /**
252
+ * 所有print方法调用前的统一入口(相当于“父类逻辑”):
253
+ * 状态切换时打印换行,并记录当前状态。
254
+ * @returns 是否发生了状态切换
255
+ */
256
+ preparePrint(type) {
257
+ if (this.prevPrintType === type) return false;
258
+ process.stdout.write("\n");
259
+ this.prevPrintType = type;
260
+ return true;
261
+ }
262
+ /**
263
+ * 为文本添加ANSI颜色;非TTY输出(重定向/管道)时返回原文本,避免日志出现转义码。
264
+ * @param text 原始文本
265
+ * @param ansiCode ANSI颜色代码,如 "90"(亮黑,大多数终端显示为灰色)
266
+ */
267
+ colorize(text, ansiCode) {
268
+ if (!process.stdout.isTTY) return text;
269
+ return `\x1b[${ansiCode}m${text}\x1b[0m`;
270
+ }
271
+ /** 流式打印AI思考内容(灰色) */
272
+ printReasoning(delta) {
273
+ if (this.preparePrint("reasoning_delta")) delta = `[思考内容] ${delta.replaceAll("\n", "")}`;
274
+ process.stdout.write(this.colorize(delta, "90"));
275
+ }
276
+ /** 流式打印AI回复内容 */
277
+ printContent(delta) {
278
+ this.preparePrint("content_delta");
279
+ process.stdout.write(delta);
280
+ }
281
+ /** 打印工具调用 */
282
+ printToolCall(name, args) {
283
+ this.preparePrint("tool_call");
284
+ process.stdout.write(`[工具调用:${name}] ${args}`);
285
+ }
286
+ /** 打印工具结果 */
287
+ printToolResult(name, result) {
288
+ this.preparePrint("tool_result");
289
+ process.stdout.write(this.colorize(`[工具结果:${name}] ${result}`, "90"));
290
+ }
291
+ /** 打印轮次结束原因、token消耗 */
292
+ printFinish(finishReason, usage) {
293
+ this.preparePrint("done");
294
+ process.stdout.write(this.colorize(`[本轮结束:${finishReason}] ${usage ? `输入${usage.prompt_tokens}tok,输出${usage.completion_tokens}tok,总共${usage.total_tokens}tok` : ""}${finishReason === "stop" ? "\n\n" : "\n"}`, "90"));
295
+ }
296
+ };
297
+ //#endregion
298
+ export { runAgent as a, defineTool as i, get_weather_default as n, stream as o, defineModel as r, TUI as t };
299
+
300
+ //# sourceMappingURL=tui-Dz0id9sE.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tui-Dz0id9sE.mjs","names":[],"sources":["../src/llm.ts","../src/agent.ts","../src/helper.ts","../src/tools/get-weather.ts","../src/tui.ts"],"sourcesContent":["// ================================\n// 解析OpenAI API Compatible的SSE事件流\n// ================================\n\nimport { fetcher, parseSSE, pick, to } from \"@nickyzj2023/utils\";\nimport type {\n\tChatCompletionsChunk,\n\tFinishReason,\n\tLLMEvent,\n\tMessage,\n\tModel,\n\tToolCall,\n\tToolDefinition,\n\tUsage,\n} from \"./types.js\";\n\n/**\n * 分离ToolDefinition中的valid/invalid字段,前者可以传给模型,后者用于本地运算\n * @returns [validObj, invalidObj]\n */\nconst detachToolArguments = (toolDefinition: ToolDefinition) => {\n\treturn [\n\t\tpick(toolDefinition, [\"type\", \"function\"]),\n\t\tpick(toolDefinition, [\"execute\"]),\n\t] as const;\n};\n\n/**\n * 从消息中提取模型的思考内容。\n * 不同供应商,即使都用OpenAI API Compatible接口,输出的思考内容字段也可能不同,例如:\n * - OpenRouter里的思考字段为reasoning\n * - 火山引擎的叫reasoning_content\n * @returns 统一返回reasoning作为思考内容字段\n */\nconst extractReasoning = (msgLike: Record<string, any>): string | undefined => {\n\treturn msgLike.reasoning || msgLike.reasoning_content;\n};\n\n/**\n * 流式请求模型,转发\n */\nexport async function* stream(\n\tmodel: Model,\n\tmessages: Message[],\n\ttools: ToolDefinition[] = [],\n): AsyncGenerator<LLMEvent> {\n\t// 剥离ToolDefinition里的私有字段/语法糖\n\tconst validTools = tools.map((tool) => detachToolArguments(tool)[0]);\n\n\tconst api = fetcher(model.baseUrl, {\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${model.apiKey}`,\n\t\t},\n\t\t// 覆盖默认返回的res.json(),改用野生Response\n\t\tparser: async (res) => res,\n\t});\n\n\t// 发出请求\n\tconst [error, response] = await to(\n\t\tapi.post<Response>(\"/chat/completions\", {\n\t\t\tstream: true,\n\t\t\tmodel: model.model,\n\t\t\tmessages,\n\t\t\ttools: validTools,\n\t\t}),\n\t);\n\tif (error) {\n\t\tyield { type: \"error\", message: error.message };\n\t\treturn;\n\t}\n\n\t// 逐行解析SSE事件\n\tconst toolCallBuffers = new Map<number, ToolCall>();\n\tlet usage: Usage | undefined;\n\tlet finishReason: FinishReason = null;\n\tfor await (const chunk of parseSSE<ChatCompletionsChunk>(response)) {\n\t\t// 字符串(通常是\"[DONE]\"),无需处理\n\t\tif (typeof chunk === \"string\") {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (chunk.usage) {\n\t\t\tusage = chunk.usage;\n\t\t}\n\n\t\t// 模型无回复,暂不处理\n\t\tconst choice = chunk.choices?.[0];\n\t\tif (!choice) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst { delta } = choice;\n\t\tconst { content: contentDelta, tool_calls: toolCalls } = delta;\n\n\t\t// 模型祈祷中...\n\t\tconst reasoning = extractReasoning(delta);\n\t\tif (reasoning) {\n\t\t\tyield { type: \"reasoning_delta\", delta: reasoning };\n\t\t}\n\n\t\t// 模型确定回复\n\t\tif (contentDelta) {\n\t\t\tyield { type: \"content_delta\", delta: contentDelta.toString() };\n\t\t}\n\n\t\t// 拼接工具调用请求\n\t\tif (toolCalls) {\n\t\t\tfor (const call of toolCalls) {\n\t\t\t\tconst {\n\t\t\t\t\tindex = 0,\n\t\t\t\t\ttype = \"function\",\n\t\t\t\t\tid,\n\t\t\t\t\tfunction: fn,\n\t\t\t\t\t...extra\n\t\t\t\t} = call;\n\n\t\t\t\tconst existing = toolCallBuffers.getOrInsert(index, {\n\t\t\t\t\tid: \"\",\n\t\t\t\t\ttype,\n\t\t\t\t\tfunction: { name: \"\", arguments: \"\" },\n\t\t\t\t});\n\n\t\t\t\tif (id) {\n\t\t\t\t\texisting.id = id;\n\t\t\t\t}\n\t\t\t\tif (fn?.name) {\n\t\t\t\t\texisting.function.name += fn.name;\n\t\t\t\t}\n\t\t\t\tif (fn?.arguments) {\n\t\t\t\t\texisting.function.arguments += fn.arguments;\n\t\t\t\t}\n\t\t\t\t// 一些厂商(Gemini)会在工具调用时要求保留CoT等额外信息\n\t\t\t\tif (extra) {\n\t\t\t\t\tObject.assign(existing, extra);\n\t\t\t\t}\n\n\t\t\t\ttoolCallBuffers.set(index, existing);\n\t\t\t}\n\t\t}\n\n\t\tif (choice.finish_reason) {\n\t\t\tfinishReason = choice.finish_reason;\n\t\t}\n\t}\n\n\t// 流式传输结束:\n\t// 1. 依次发起工具调用\n\tfor (const [, call] of toolCallBuffers) {\n\t\tyield {\n\t\t\ttype: \"tool_call\",\n\t\t\tid: call.id,\n\t\t\tname: call.function.name,\n\t\t\targs: call.function.arguments,\n\t\t};\n\t}\n\t// 2. 发出done事件\n\tyield { type: \"done\", finishReason, usage };\n}\n","// ================================\n// Agent Loop,即Re-Act:用户输入 -> while(模型思考 <-> Agent帮模型调用外部工具) -> 模型输出\n// ================================\n\nimport { to } from \"@nickyzj2023/utils\";\nimport { stream } from \"./llm.js\";\nimport type {\n\tAgentEvent,\n\tMessage,\n\tModel,\n\tToolCall,\n\tToolDefinition,\n} from \"./types.js\";\n\nexport async function* runAgent(\n\tmodel: Model,\n\tmessages: Message[],\n\ttools: ToolDefinition[],\n): AsyncGenerator<AgentEvent> {\n\tconst toolMap = new Map(tools.map((tool) => [tool.function.name, tool]));\n\n\twhile (true) {\n\t\t// 1. 流式调用大模型,收集回复内容\n\t\tlet content = \"\";\n\t\tconst toolCalls: ToolCall[] = [];\n\n\t\tfor await (const e of stream(model, messages, tools)) {\n\t\t\tswitch (e.type) {\n\t\t\t\tcase \"reasoning_delta\": {\n\t\t\t\t\tyield e;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase \"content_delta\": {\n\t\t\t\t\tcontent += e.delta;\n\t\t\t\t\tyield e;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase \"tool_call\": {\n\t\t\t\t\ttoolCalls.push({\n\t\t\t\t\t\tid: e.id,\n\t\t\t\t\t\ttype: \"function\",\n\t\t\t\t\t\tfunction: {\n\t\t\t\t\t\t\tname: e.name,\n\t\t\t\t\t\t\targuments: e.args,\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t\tyield e;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase \"done\": {\n\t\t\t\t\tyield e;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// 2. 把模型的回复推入上下文\n\t\tconst message: Message = {\n\t\t\trole: \"assistant\",\n\t\t\tcontent,\n\t\t};\n\t\tif (toolCalls.length > 0) {\n\t\t\tmessage.tool_calls = toolCalls;\n\t\t}\n\t\tmessages.push(message);\n\n\t\t// 3. 如果没有工具调用,则结束循环\n\t\tif (toolCalls.length === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\t// 4. 调用工具\n\t\tfor (const call of toolCalls) {\n\t\t\tconst { name, arguments: args } = call.function;\n\t\t\tconst tool = toolMap.get(name);\n\n\t\t\tlet result = \"\";\n\t\t\tif (!tool) {\n\t\t\t\tresult = `不存在工具“${name}”`;\n\t\t\t} else {\n\t\t\t\tconst [error, response] = await to(tool.execute(JSON.parse(args)));\n\t\t\t\tresult = error\n\t\t\t\t\t? `工具“${name}”执行出错:${error.message}`\n\t\t\t\t\t: String(response);\n\t\t\t}\n\n\t\t\tmessages.push({\n\t\t\t\trole: \"tool\",\n\t\t\t\ttool_call_id: call.id,\n\t\t\t\tcontent: result,\n\t\t\t});\n\t\t\tyield { type: \"tool_result\", id: call.id, name: name, result };\n\t\t}\n\t}\n}\n","// ================================\n// 外部项目使用本npm包时的便捷方法\n// ================================\n\nimport type { Model, ToolDefinition } from \"./types.js\";\n\n/**\n * 辅助定义一个POST /chat/completions支持的model参数\n * @remarks 只有baseUrl字段是必须的\n */\nexport const defineModel = (config: Model): Model => ({\n\tmodalities: [\"text\"],\n\tcontext: 131072,\n\t...config,\n});\n\n/**\n * 辅助定义一个POST /chat/completions支持的tool对象\n * @param execute 实际执行工具的函数\n */\nexport const defineTool = (\n\tname: ToolDefinition[\"function\"][\"name\"],\n\tdescription: ToolDefinition[\"function\"][\"description\"],\n\tproperties: ToolDefinition[\"function\"][\"parameters\"][\"properties\"],\n\texecute: ToolDefinition[\"execute\"],\n): ToolDefinition => {\n\t// 收集property内部填写的required: true语法糖,推到外面的required数组\n\tconst _required: string[] = [];\n\tconst _properties = Object.entries(properties).reduce(\n\t\t(result, [key, property]) => {\n\t\t\tif (\"required\" in property) {\n\t\t\t\t_required.push(key);\n\t\t\t\tdelete property.required;\n\t\t\t}\n\t\t\tresult[key] = property;\n\t\t\treturn result;\n\t\t},\n\t\t{} as Omit<\n\t\t\tToolDefinition[\"function\"][\"parameters\"][\"properties\"],\n\t\t\t\"required\"\n\t\t>,\n\t);\n\n\treturn {\n\t\ttype: \"function\",\n\t\tfunction: {\n\t\t\tname,\n\t\t\tdescription,\n\t\t\tparameters: {\n\t\t\t\ttype: \"object\",\n\t\t\t\tproperties: _properties,\n\t\t\t\trequired: _required,\n\t\t\t},\n\t\t},\n\t\texecute,\n\t};\n};\n","import { fetcher } from \"@nickyzj2023/utils\";\nimport { defineTool } from \"../helper.js\";\n\nexport default defineTool(\n\t\"get_weather\",\n\t\"查询指定城市的天气情况\",\n\t{\n\t\tcity: {\n\t\t\ttype: \"string\",\n\t\t\tdescription: \"城市名,如shanghai、tokyo\",\n\t\t\trequired: true,\n\t\t},\n\t},\n\tasync ({ city }) => {\n\t\tconst api = fetcher(\"https://wttr.in\", {\n\t\t\tparams: {\n\t\t\t\tformat: \"j1\", // 返回JSON格式\n\t\t\t},\n\t\t});\n\t\tconst data = await api.get<any>(`/${city}`);\n\t\tconst nearestArea = data.nearest_area[0];\n\t\tconst currentCondition = data.current_condition[0];\n\t\treturn [\n\t\t\t`位置:${nearestArea.country[0].value}-${nearestArea.region[0].value}-${nearestArea.areaName[0].value}`,\n\t\t\t`当前温度: ${currentCondition.temp_C}°C`,\n\t\t\t`体感温度: ${currentCondition.FeelsLikeC}°C`,\n\t\t\t`天气状况: ${currentCondition.weatherDesc[0].value}`,\n\t\t\t`湿度: ${currentCondition.humidity}%`,\n\t\t].join(\"\\n\");\n\t},\n);\n","// ================================\n// 终端UI\n// ================================\n\nimport readline from \"node:readline\";\nimport type { AgentEvent, FinishReason, Usage } from \"./types.js\";\n\nexport class TUI {\n\tprivate rl: readline.Interface | null = null;\n\tprivate onPrompt: ((prompt: string) => void | Promise<void>) | null = null;\n\n\t/** 是否允许输入 */\n\tprivate isBusy = false;\n\n\t/** 上次打印内容所属的状态(reasoning/content/tool)\n\t * 用于在新的状态开始时改变样式、打印前缀\n\t */\n\tprivate prevPrintType: AgentEvent[\"type\"] | undefined = undefined;\n\n\t/**\n\t * 实例化TUI时,接收一个“发出用户提示词”的回调函数\n\t * UI只做UI的事,提示词发给谁让调用方决定\n\t */\n\tconstructor(onPrompt: (prompt: string) => void | Promise<void>) {\n\t\tthis.onPrompt = onPrompt;\n\t}\n\n\t/** 启动TUI */\n\tstart() {\n\t\tthis.rl = readline.createInterface({\n\t\t\tinput: process.stdin,\n\t\t\toutput: process.stdout,\n\t\t});\n\t\tthis.prompting();\n\n\t\t// 停止TUI后,清理残留的监听事件\n\t\tthis.rl.on(\"close\", () => {\n\t\t\tthis.rl?.close();\n\t\t\tthis.rl = null;\n\t\t});\n\t}\n\n\t/** 监听用户输入 */\n\tprivate prompting() {\n\t\tif (this.isBusy) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.rl?.question(\"> \", async (answer) => {\n\t\t\tconst input = answer.trim();\n\t\t\t// 如果输入为空,则重新question\n\t\t\tif (!input) {\n\t\t\t\tthis.prompting();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tthis.isBusy = true;\n\t\t\tawait this.onPrompt?.(input);\n\t\t\tthis.isBusy = false;\n\n\t\t\tthis.prompting();\n\t\t});\n\t}\n\n\t/**\n\t * 所有print方法调用前的统一入口(相当于“父类逻辑”):\n\t * 状态切换时打印换行,并记录当前状态。\n\t * @returns 是否发生了状态切换\n\t */\n\tprivate preparePrint(type: AgentEvent[\"type\"]): boolean {\n\t\tif (this.prevPrintType === type) {\n\t\t\treturn false;\n\t\t}\n\t\tprocess.stdout.write(\"\\n\");\n\t\tthis.prevPrintType = type;\n\t\treturn true;\n\t}\n\n\t/**\n\t * 为文本添加ANSI颜色;非TTY输出(重定向/管道)时返回原文本,避免日志出现转义码。\n\t * @param text 原始文本\n\t * @param ansiCode ANSI颜色代码,如 \"90\"(亮黑,大多数终端显示为灰色)\n\t */\n\tprivate colorize(text: string, ansiCode: string): string {\n\t\tif (!process.stdout.isTTY) {\n\t\t\treturn text;\n\t\t}\n\t\treturn `\\x1b[${ansiCode}m${text}\\x1b[0m`;\n\t}\n\n\t/** 流式打印AI思考内容(灰色) */\n\tprintReasoning(delta: string) {\n\t\tif (this.preparePrint(\"reasoning_delta\")) {\n\t\t\tdelta = `[思考内容] ${delta.replaceAll(\"\\n\", \"\")}`;\n\t\t}\n\t\tprocess.stdout.write(this.colorize(delta, \"90\"));\n\t}\n\n\t/** 流式打印AI回复内容 */\n\tprintContent(delta: string) {\n\t\tthis.preparePrint(\"content_delta\");\n\t\tprocess.stdout.write(delta);\n\t}\n\n\t/** 打印工具调用 */\n\tprintToolCall(name: string, args: any) {\n\t\tthis.preparePrint(\"tool_call\");\n\t\tprocess.stdout.write(`[工具调用:${name}] ${args}`);\n\t}\n\n\t/** 打印工具结果 */\n\tprintToolResult(name: string, result: string) {\n\t\tthis.preparePrint(\"tool_result\");\n\t\tprocess.stdout.write(this.colorize(`[工具结果:${name}] ${result}`, \"90\"));\n\t}\n\n\t/** 打印轮次结束原因、token消耗 */\n\tprintFinish(finishReason: FinishReason, usage?: Usage) {\n\t\tthis.preparePrint(\"done\");\n\t\tprocess.stdout.write(\n\t\t\tthis.colorize(\n\t\t\t\t`[本轮结束:${finishReason}] ${usage ? `输入${usage.prompt_tokens}tok,输出${usage.completion_tokens}tok,总共${usage.total_tokens}tok` : \"\"}${finishReason === \"stop\" ? \"\\n\\n\" : \"\\n\"}`,\n\t\t\t\t\"90\",\n\t\t\t),\n\t\t);\n\t}\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@nickyzj2023/ai",
3
+ "version": "1.0.0",
4
+ "description": "我的“pi”,参考了pi-from-scratch",
5
+ "type": "module",
6
+ "main": "./dist/index.mjs",
7
+ "types": "./dist/index.d.mts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.mts",
11
+ "import": "./dist/index.mjs"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "bin": {
18
+ "ai": "./dist/cli.mjs"
19
+ },
20
+ "keywords": [
21
+ "agent",
22
+ "llm",
23
+ "openai",
24
+ "react",
25
+ "tool-calling",
26
+ "streaming",
27
+ "cli",
28
+ "tui"
29
+ ],
30
+ "author": "nickyzj2023",
31
+ "license": "ISC",
32
+ "engines": {
33
+ "node": ">=26.0.0"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "dependencies": {
39
+ "@nickyzj2023/utils": "^1.0.92"
40
+ },
41
+ "devDependencies": {
42
+ "@biomejs/biome": "^2.5.11",
43
+ "@types/node": "^26.4.0",
44
+ "tsdown": "^0.22.14",
45
+ "typescript": "^7.0.2"
46
+ },
47
+ "scripts": {
48
+ "build": "tsdown",
49
+ "check": "biome check --diagnostic-level=error --write src/",
50
+ "typecheck": "tsc --noEmit"
51
+ }
52
+ }