@nickyzj2023/utils 1.0.77 → 1.0.79

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/index.d.mts CHANGED
@@ -13,7 +13,7 @@ declare namespace AI {
13
13
  /**
14
14
  * 模型的最大上下文(输入+输出)
15
15
  * @default 128000
16
- * @remarks 会在调用chatCompletions后智能调用compact(如当前上下文>最大上下文*80%时),可以在chatCompletions里自定义compact执行时机 */
16
+ * @remarks 会在调用chatCompletions后智能调用compact(如当前上下文>最大上下文*80%时),详见chatCompletions.autoCompact配置 */
17
17
  context?: number;
18
18
  };
19
19
  type InputType = "text" | "image" | "video" | "audio" | "file";
@@ -83,7 +83,8 @@ declare namespace ChatCompletions {
83
83
  type Options = {
84
84
  stream?: boolean;
85
85
  tools?: AI.ToolDefinition[]; /** 工具运行结束后(无论成功失败)的回调,可用于打印日志 */
86
- onToolHandled?: (name: string, args: string, result: any) => void;
86
+ onToolHandled?: (name: string, args: string, result: any) => void; /** 自动压缩上下文 */
87
+ autoCompact?: {};
87
88
  };
88
89
  /** 非流式POST /chat/completions的响应结果 */
89
90
  type NonStreamResponse = {
@@ -137,14 +138,42 @@ declare namespace ChatCompletions {
137
138
  //#endregion
138
139
  //#region src/ai/chatCompletions/index.d.ts
139
140
  /**
140
- * 兼容OpenAI API的聊天补全函数
141
+ * 兼容OpenAI API的聊天补全函数(流式模式)
141
142
  * - 自动处理工具调用
142
- * - 支持普通/流式响应
143
+ * - 传入 `stream: true` 时返回异步迭代器,逐块产出内容与 usage
143
144
  *
144
145
  * @param model 模型配置,包含model、baseUrl、apiKey
145
146
  * @param messages OpenAI API兼容的消息数组
146
- * @param extraBody 可选的额外参数,如tools、temperature、stream
147
- * @returns 普通模式下返回`{ content, usage, ... }`;`stream: true`时返回异步迭代器
147
+ * @param options 额外参数,须包含 `stream: true`,也可含 tools、temperature
148
+ * @returns 异步迭代器,逐块产出 `{ content?, reasoning?, usage? }`
149
+ *
150
+ * @example
151
+ * // 流式传输
152
+ * const result = await chatCompletions(
153
+ * { baseUrl: "http://127.0.0.1:11434/v1" },
154
+ * [{ role: "user", content: "你好" }],
155
+ * { stream: true },
156
+ * );
157
+ * for await (const { content, usage } of result) {
158
+ * if (content) {
159
+ * console.log("流式传输中:", content);
160
+ * } else if (usage) {
161
+ * console.log("对话结束,消耗:", usage);
162
+ * }
163
+ * }
164
+ */
165
+ declare function chatCompletions(model: AI.Model, messages: AI.Message[], options: ChatCompletions.Options & {
166
+ stream: true;
167
+ }): Promise<AsyncGenerator<ChatCompletions.StreamChunk>>;
168
+ /**
169
+ * 兼容OpenAI API的聊天补全函数(普通模式)
170
+ * - 自动处理工具调用
171
+ * - 默认返回完整结果,非流式
172
+ *
173
+ * @param model 模型配置,包含model、baseUrl、apiKey
174
+ * @param messages OpenAI API兼容的消息数组
175
+ * @param options 可选的额外参数,如tools、temperature等
176
+ * @returns `{ content, usage, ... }` 完整结果
148
177
  *
149
178
  * @example
150
179
  * // 最简调用
@@ -155,12 +184,12 @@ declare namespace ChatCompletions {
155
184
  * );
156
185
  * console.log(reasoning); // "The user said..."
157
186
  * console.log(content); // "你好!有什么我可以帮你的吗?"
158
- * console.log(usage); // { prompt_tokens: 13, completion_tokens: 9, total_tokens: 22 }
187
+ * console.log(usage); // { prompt_tokens: [redacted], completion_tokens: [redacted], total_tokens: [redacted] }
159
188
  *
160
189
  * @example
161
190
  * // 工具调用
162
191
  * const { content, usage } = await chatCompletions(
163
- * { baseUrl: "http://127.0.0.1:11434/v1", model: "model.gguf", apiKey: "sk-local-no-need-key" },
192
+ * { baseUrl: "http://127.0.0.1:11434/v1", model: "model.gguf", apiKey: "sk-loc**********-key" },
164
193
  * [{ role: "user", content: "查询上海天气" }],
165
194
  * {
166
195
  * tools: [{
@@ -174,27 +203,59 @@ declare namespace ChatCompletions {
174
203
  * }],
175
204
  * },
176
205
  * );
177
- *
178
- * @example
179
- * // 流式传输
180
- * const result = await chatCompletions(
181
- * { baseUrl: "http://127.0.0.1:11434/v1" },
182
- * [{ role: "user", content: "你好" }],
183
- * { stream: true },
184
- * );
185
- * for await (const { content, usage } of result) {
186
- * if (content) {
187
- * console.log("流式传输中:", content);
188
- * } else if (usage) {
189
- * console.log("对话结束,消耗:", usage);
190
- * }
191
- * }
192
206
  */
193
- declare function chatCompletions(model: AI.Model, messages: AI.Message[], options: ChatCompletions.Options & {
194
- stream: true;
195
- }): Promise<AsyncGenerator<ChatCompletions.StreamChunk>>;
196
207
  declare function chatCompletions(model: AI.Model, messages: AI.Message[], options?: ChatCompletions.Options): Promise<ChatCompletions.NonStreamResult>;
197
208
  //#endregion
209
+ //#region src/ai/compact/types.d.ts
210
+ declare namespace Compact {
211
+ type ReplacerOfToolResultContent = (content: AI.Message["content"]) => AI.Message["content"];
212
+ type ReplacerOfMediaContent = (content: AI.Message["content"]) => AI.Message["content"];
213
+ type SummarizeOptions = {
214
+ /** 不总结最新的X%条消息 */keepPercent: number; /** 用什么模型总结 */
215
+ model: AI.Model; /** 用于指导大模型如何总结消息的提示词 */
216
+ systemPrompt: string;
217
+ };
218
+ }
219
+ //#endregion
220
+ //#region src/ai/compact/index.d.ts
221
+ /**
222
+ * 自动优化上下文,类似AI Coding Agent的/compact命令
223
+ */
224
+ declare const compactMessages: (messages: AI.Message[], model: AI.Model, options?: {
225
+ /** 提供token消耗情况时,能更准确地判断上下文是否达到阈值 */usage?: ChatCompletions.Usage;
226
+ /**
227
+ * 上下文>总上下文*ratio时压缩工具调用结果
228
+ * @default 0.6
229
+ */
230
+ ratioOfCompactToolResult?: number;
231
+ /**
232
+ * 如何压缩工具调用结果,例如让其他模型返回精简后的工具结果
233
+ * @default (content) => "(已被消费)"
234
+ */
235
+ replacerOfToolResultContent?: Compact.ReplacerOfToolResultContent;
236
+ /**
237
+ * 上下文>总上下文*ratio时压缩图片/音频/视频消息
238
+ * @default 0.7
239
+ */
240
+ ratioOfCompactMedia?: number;
241
+ /**
242
+ * 如何压缩媒体消息,例如让其他模型用自然语言简短描述一遍
243
+ * @default (content) => "(已被丢弃)"
244
+ */
245
+ replacerOfMediaContent?: Compact.ReplacerOfMediaContent;
246
+ /**
247
+ * 上下文>总上下文*ratio时总结消息
248
+ * @default 0.8
249
+ * @remarks 如果总结成功,会把summarizeOptions.keepPercent(默认0.2(20%))以外的消息压成一条消息;如果总结失败,会采取兜底压缩方法:硬删除summarizeOptions.keepPercent以外的消息
250
+ */
251
+ ratioOfSummarize?: number;
252
+ /**
253
+ * 总结消息时的配置项
254
+ * @default { keepPercent: 0.2, model: undefined, systemPrompt: "总结历史消息" }
255
+ */
256
+ summarizeOptions?: Compact.SummarizeOptions;
257
+ }) => Promise<void>;
258
+ //#endregion
198
259
  //#region src/ai/helper.d.ts
199
260
  /**
200
261
  * 辅助定义一个POST /chat/completions支持的model参数
@@ -210,6 +271,13 @@ declare const defineTool: (name: AI.ToolDefinition["function"]["name"], descript
210
271
  * 从GET /models获取模型名称
211
272
  */
212
273
  declare const getModelName: (baseUrl: string) => Promise<string>;
274
+ /**
275
+ * 根据上下文里的中/英文/多模态消息,估算出可能消耗的token
276
+ * - 单词 ≈ 1.5token
277
+ * - 标点/空白等非词字符每 4 个 ≈ 1token
278
+ * - 图片/音频/视频/文件 ≈ 10000token(不好估算,取个较大的值)
279
+ */
280
+ declare const estimateTokens: (messages?: AI.Message[]) => number;
213
281
  //#endregion
214
282
  //#region src/dom/logger.d.ts
215
283
  /**
@@ -230,18 +298,11 @@ interface LoggerOptions {
230
298
  /**
231
299
  * 带额外信息的 console.log
232
300
  *
233
- * **直接调用**:使用默认选项打印消息
234
- * @param messages - 日志消息,支持多条
235
- *
236
301
  * **预配置**:先传入选项返回 logger 函数,再调用打印
237
302
  * @param options - 配置选项
238
303
  * @returns 配置后的 logger 函数
239
304
  *
240
305
  * @example
241
- * // 直接调用(默认显示时间和文件名)
242
- * logger("调试信息"); // "[14:30:00] [index.ts:15:2] 调试信息"
243
- * logger("消息1", "消息2"); // "[14:30:00] [index.ts:15:2] 消息1 消息2"
244
- *
245
306
  * // 预配置后调用
246
307
  * const myLogger = logger({ withTime: true, withFileName: true });
247
308
  * myLogger("一段消息", "另一段消息"); // "[18:59:47] [index.ts:137:2] 一段消息 另一段消息"
@@ -251,6 +312,17 @@ interface LoggerOptions {
251
312
  * plainLogger("纯文件名前缀"); // "[index.ts:15:2] 纯文件名前缀"
252
313
  */
253
314
  declare function logger(options?: LoggerOptions): (...messages: any[]) => void;
315
+ /**
316
+ * 带额外信息的 console.log
317
+ *
318
+ * **直接调用**:使用默认选项打印消息
319
+ * @param messages - 日志消息,支持多条
320
+ *
321
+ * @example
322
+ * // 直接调用(默认显示时间和文件名)
323
+ * logger("调试信息"); // "[14:30:00] [index.ts:15:2] 调试信息"
324
+ * logger("消息1", "消息2"); // "[14:30:00] [index.ts:15:2] 消息1 消息2"
325
+ */
254
326
  declare function logger(...messages: any[]): void;
255
327
  //#endregion
256
328
  //#region src/function/loop-until.d.ts
@@ -841,4 +913,4 @@ declare const sleep: (time?: number) => Promise<unknown>;
841
913
  */
842
914
  declare const throttle: <T extends (...args: any[]) => any>(fn: T, delay?: number) => (this: any, ...args: Parameters<T>) => void;
843
915
  //#endregion
844
- export { AI, CamelToSnake, Capitalize, type ChatCompletions, Decapitalize, DeepMapKeys, DeepMapValues, ImageCompressionOptions, LockQueue, LoggerOptions, Primitive, RequestInit, SetTtl, SnakeToCamel, camelToSnake, capitalize, chatCompletions, compactStr, debounce, decapitalize, defineModel, defineTool, extractErrorMessage, fetcher, getModelName, getRealURL, imageUrlToBase64, isNil, isObject, isPrimitive, logger, loopUntil, mapKeys, mapValues, mergeObjects, omit, omitBy, parseSSE, pick, pickBy, qs, randomInt, sleep, snakeToCamel, throttle, to, withCache };
916
+ export { AI, CamelToSnake, Capitalize, type ChatCompletions, type Compact, Decapitalize, DeepMapKeys, DeepMapValues, ImageCompressionOptions, LockQueue, LoggerOptions, Primitive, RequestInit, SetTtl, SnakeToCamel, camelToSnake, capitalize, chatCompletions, compactMessages, compactStr, debounce, decapitalize, defineModel, defineTool, estimateTokens, extractErrorMessage, fetcher, getModelName, getRealURL, imageUrlToBase64, isNil, isObject, isPrimitive, logger, loopUntil, mapKeys, mapValues, mergeObjects, omit, omitBy, parseSSE, pick, pickBy, qs, randomInt, sleep, snakeToCamel, throttle, to, withCache };
package/dist/index.mjs CHANGED
@@ -2,6 +2,6 @@ const e=e=>e==null,t=e=>e?.constructor===Object,n=e=>e==null||typeof e!=`object`
2
2
  `)),a=r?a.replace(/\r?\n/g,` `):a.replace(/\r?\n/g,`\\n`),a=a.replace(/\s+/g,` `).trim(),n>0&&a.length>n?`${a.slice(0,n)}...`:a},h=e=>{if(e instanceof Error)return e.message;if(typeof e==`string`)return e;if(t(e)){let t=e.message||e.msg;if(t)return t;for(let t of Object.values(e)){let e=h(t);if(e)return e}}return JSON.stringify(e,null,2)},g={parse:e=>{let t=new URLSearchParams(e),n={};for(let[e,r]of t)Number.isNaN(Number(r))?n[e]=r:n[e]=Number(r);return n},stringify:(e,t)=>{let{addQueryPrefix:n=!1}=t??{},r=new URLSearchParams(e).toString();return r?n?`?${r}`:r:``}},_=(n=``,r={})=>{let i=async(i,o={})=>{let s=new URL(n?`${n}${i}`:i),{params:c,parser:l,...u}=a(r,o);t(c)&&Object.entries(c).forEach(([t,n])=>{e(n)||s.searchParams.append(t,n.toString())}),(t(u.body)||Array.isArray(u.body))&&(u.body=JSON.stringify(u.body),u.headers={...u.headers,"Content-Type":`application/json`});let d=await fetch(s,u);if(!d.ok){let e=d.statusText||await d.text();try{e=h(JSON.parse(e))}catch{}throw Error(e)}return await(l?.(d)??d.json())};return{get:(e,t)=>i(e,{...t,method:`GET`}),post:(e,t,n)=>i(e,{...n,method:`POST`,body:t}),put:(e,t,n)=>i(e,{...n,method:`PUT`,body:t}),delete:(e,t)=>i(e,{...t,method:`DELETE`})}},v=async e=>{try{return[null,await e]}catch(e){return[e,void 0]}},y=async e=>{let[t,n]=await v(fetch(e,{method:`HEAD`,redirect:`manual`}));return t?e:n.headers.get(`location`)||e},b=e=>{let t=new Uint8Array(e),n=``;for(let e=0;e<t.byteLength;e++)n+=String.fromCharCode(t[e]);return btoa(n)},x=async(e,t={})=>{let{quality:n=.92,compressor:r,fetcher:i=fetch}=t;if(!e.startsWith(`http`))throw Error(`图片地址必须以http或https开头`);let a=await i(e);if(!a.ok)throw Error(`获取图片失败: ${a.statusText}`);let o=a.headers.get(`Content-Type`)||`image/jpeg`,s=await a.arrayBuffer();if(o!==`image/jpeg`&&o!==`image/png`)return`data:${o};base64,${b(s)}`;if(r)return await r(s,o,n);if(typeof OffscreenCanvas<`u`){let e=null;try{let t=new Blob([s],{type:o});e=await createImageBitmap(t);let r=new OffscreenCanvas(e.width,e.height),i=r.getContext(`2d`);if(!i)throw Error(`无法获取 OffscreenCanvas context`);return i.drawImage(e,0,0),e.close(),e=null,`data:${o};base64,${b(await(await r.convertToBlob({type:o,quality:n})).arrayBuffer())}`}catch{return e?.close(),`data:${o};base64,${b(s)}`}}return`data:${o};base64,${b(s)}`};async function*S(e){let t=e.body?.getReader();if(!t)return;let n=new TextDecoder,r=``;try{for(;;){let{value:e,done:i}=await t.read();if(i)break;r+=n.decode(e,{stream:!0});let a=r.split(`
3
3
 
4
4
  `);r=a.pop()||``;for(let e of a){let t=e.split(`
5
- `);for(let e of t){if(!e.startsWith(`data:`))continue;let t=e.replace(/^data:\s*/,``).trim();try{yield JSON.parse(t)}catch{yield t}}}}}finally{t.releaseLock()}}const C=e=>({inputs:[`text`],context:128e3,...e}),w=(e,t,n,r)=>{let i=[];return{type:`function`,function:{name:e,description:t,parameters:{type:`object`,properties:l(n,e=>e===`required`?(i.push(e),!1):!0),required:i}},handler:r}},T=async e=>{let t=(await _(e).get(`/v1/models`)).data[0]?.id;if(!t)throw Error(`无法从/models获取模型名称`);return t},E=e=>[c(e,[`type`,`function`]),c(e,[`handler`])],D=async(e,t,n)=>{let r=t.find(t=>t.function.name===e.function.name)?.handler;if(!r)return`没有找到工具“${e.function.name}”的处理函数`;try{let t=await r(JSON.parse(e.function.arguments),n);return typeof t==`string`?t:JSON.stringify(t)}catch(t){if(t instanceof Error&&t.name===e.function.name)throw t;return`工具“${e.function.name}”处理失败:${h(t)}`}},O=e=>e.reasoning||e.reasoning_content,k=e=>typeof e==`string`?e:e.filter(e=>e.type===`text`).map(e=>e.text).join(`
6
- `),A=e=>{switch(e.type){case`text`:return`text`;case`image_url`:return`image`;case`input_audio`:return`audio`;case`video_url`:return`video`}},j=(e,t)=>{let{inputs:n=[`text`]}=e;return t.some(e=>{let{content:t}=e;if(typeof t!=`string`){if(Array.isArray(t))for(let e of t){let t=A(e);return!t||!n.includes(t)}return!0}})},M=async(e,t,n=[],r)=>{let{onToolHandled:i}=r??{};for(;;){let{choices:r,usage:a,...o}=await e.post(`/chat/completions`,{}),{message:s}=r?.[0]??{};if(!s)throw Error(`模型没有回复任何内容`);t.push(s);let{content:c=``,tool_calls:l=[],...u}=s,d=O(s);if(l.length>0&&n.length>0){for(let e of l){let r=await D(e,n,{messages:t});t.push({role:`tool`,content:r,tool_call_id:e.id}),i?.(e.function.name,e.function.arguments,r)}continue}return{content:k(c),reasoning:d,usage:a,...o,...u}}},N=async function*(e,t,n=[],r){let{onToolHandled:i}=r??{};for(;;){let r=new Map,a=``,o=null,s,c=await e.post(`/chat/completions`,{stream:!0},{parser:async e=>e});for await(let e of S(c)){e.usage&&(s=e.usage);let t=e.choices?.[0];if(!t)continue;let{delta:n}=t,{content:i,tool_calls:c}=n,l=O(n);if(l&&(yield{reasoning:l}),i&&(a+=i,yield{content:i}),c)for(let e of c){let t=r.get(e.index)??{id:``,type:`function`,function:{name:``,arguments:``}};e.id&&(t.id=e.id),e.function?.name&&(t.function.name+=e.function.name),e.function?.arguments&&(t.function.arguments+=e.function.arguments),r.set(e.index,t)}t.finish_reason&&(o=t.finish_reason)}let l=Array.from(r.values());if(o===`tool_calls`&&l.length>0&&n.length>0){t.push({role:`assistant`,content:a,tool_calls:l});for(let e of l){let r=await D(e,n,{messages:t});t.push({role:`tool`,content:r,tool_call_id:e.id}),i?.(e.function.name,e.function.arguments,r)}continue}t.push({role:`assistant`,content:a}),s&&(yield{usage:s});break}};async function P(e,t,n){let{baseUrl:r,apiKey:i=``,model:a,customBody:o,inputs:s,...c}=e,{stream:l,tools:u=[],...d}=n??{};if(j(e,t))throw Error(`当前上下文含有模型不支持的输入类型`);let f=_(r,{headers:{Authorization:`Bearer ${i}`},body:{model:a??await T(r),messages:t,tools:u?.map(e=>E(e)[0]),...o}});return(l?N:M)(f,t,u,{...d,...c})}function F(e={},t=0){let{withTime:n=!0,withFileName:r=!0}=e;return(...e)=>{let i=[];if(n&&i.push(`[${new Date().toLocaleTimeString()}]`),r){let{stack:e}=Error(),n=(e?.split(`
7
- `)[2+t]?.trim())?.match(/^at\s+(?:.*?\s*\()?(.*?):(\d+)(?::(\d+))?\)?/);if(n?.[1]){let e=n[1].split(/[/\\]/).pop();i.push(`[${e}:${n[2]}]`)}}i.push(...e),console.log(...i)}}function I(e,...n){if(t(e)&&(`withTime`in e||`withFileName`in e))return F(e,0);F({},1)(e,...n)}const L=async(e,t)=>{let{maxRetries:n=5,shouldStop:r}=t??{},i;for(let t=0;t<n;t++)if(i=await e(t),r?.(i)===!0)return i;if(!r)return i;throw Error(`超过了最大循环次数(${n})且未满足停止执行条件`)},R=(e,t=-1)=>{let n=new Map,r=(...r)=>{let i=JSON.stringify(r),a=Date.now(),o=n.get(i);if(o&&a<o.expiresAt)return o.value;let s=t===-1?1/0:a+t*1e3,c=e.apply({setTtl:e=>{s=a+e*1e3}},r);if(c instanceof Promise){let e=c.then(e=>(n.set(i,{value:e,expiresAt:s}),e));return n.set(i,{value:e,expiresAt:s}),e}return n.set(i,{value:c,expiresAt:s}),c};return r.clear=()=>n.clear(),r.updateTtl=e=>{t=e;let r=Date.now(),i=r+e*1e3;for(let[e,t]of n.entries())t.expiresAt>r&&(t.expiresAt=i,n.set(e,t))},r},z=(e,t)=>Math.floor(Math.random()*(t-e+1))+e,B=(e,t=300)=>{let n=null;return(...r)=>{n&&clearTimeout(n),n=setTimeout(()=>{e(...r)},t)}};var V=class{queue;constructor(){this.queue=Promise.resolve()}waitInQueue(){let e,t=new Promise(t=>{e=t}),n=this.queue.then(()=>e);return this.queue=t,n}};const H=async(e=150)=>new Promise(t=>{setTimeout(t,e)}),U=(e,t=300)=>{let n=null;return function(...r){n||=setTimeout(()=>{n=null,e.apply(this,r)},t)}};export{V as LockQueue,d as camelToSnake,f as capitalize,P as chatCompletions,m as compactStr,B as debounce,p as decapitalize,C as defineModel,w as defineTool,h as extractErrorMessage,_ as fetcher,T as getModelName,y as getRealURL,x as imageUrlToBase64,e as isNil,t as isObject,n as isPrimitive,I as logger,L as loopUntil,r as mapKeys,i as mapValues,a as mergeObjects,o as omit,s as omitBy,S as parseSSE,c as pick,l as pickBy,g as qs,z as randomInt,H as sleep,u as snakeToCamel,U as throttle,v as to,R as withCache};
5
+ `);for(let e of t){if(!e.startsWith(`data:`))continue;let t=e.replace(/^data:\s*/,``).trim();try{yield JSON.parse(t)}catch{yield t}}}}}finally{t.releaseLock()}}const C=e=>({inputs:[`text`],context:128e3,...e}),w=(e,t,n,r)=>{let i=[];return{type:`function`,function:{name:e,description:t,parameters:{type:`object`,properties:l(n,e=>e===`required`?(i.push(e),!1):!0),required:i}},handler:r}},T=async e=>{let t=(await _(e).get(`/v1/models`)).data[0]?.id;if(!t)throw Error(`无法从/models获取模型名称`);return t},E=e=>{if(!e?.length)return 0;let t=new Intl.Segmenter([],{granularity:`word`}),n=e=>{let n=0,r=0;for(let i of t.segment(e))i.isWordLike?n++:r++;return Math.ceil(n*1.5+r/4)};return e.reduce((e,t)=>{let{content:r,tool_calls:i,...a}=t;if(typeof r==`string`)e+=n(r);else for(let t of r)t.type===`text`?e+=n(t.text):e+=1e4;return i&&(e+=n(JSON.stringify(i))),e+=n(JSON.stringify(a)),e},0)},D=e=>[c(e,[`type`,`function`]),c(e,[`handler`])],O=async(e,t,n)=>{let r=t.find(t=>t.function.name===e.function.name)?.handler;if(!r)return`没有找到工具“${e.function.name}”的处理函数`;try{let t=await r(JSON.parse(e.function.arguments),n);return typeof t==`string`?t:JSON.stringify(t)}catch(t){if(t instanceof Error&&t.name===e.function.name)throw t;return`工具“${e.function.name}”处理失败:${h(t)}`}},k=e=>e.reasoning||e.reasoning_content,A=e=>typeof e==`string`?e:e.filter(e=>e.type===`text`).map(e=>e.text).join(`
6
+ `),j=e=>{switch(e.type){case`text`:return`text`;case`image_url`:return`image`;case`input_audio`:return`audio`;case`video_url`:return`video`}},M=(e,t)=>{let{inputs:n=[`text`]}=e;for(let e of t){let{content:t}=e;if(typeof t!=`string`&&Array.isArray(t))for(let e of t){let t=j(e);if(!t||!n.includes(t))return t||`unknown`}}return``},N=async(e,t,n=[],r)=>{let{onToolHandled:i}=r??{};for(;;){let{choices:r,usage:a,...o}=await e.post(`/chat/completions`,{}),{message:s}=r?.[0]??{};if(!s)throw Error(`模型没有回复任何内容`);t.push(s);let{content:c=``,tool_calls:l=[],...u}=s,d=k(s);if(l.length>0&&n.length>0){for(let e of l){let r=await O(e,n,{messages:t});t.push({role:`tool`,content:r,tool_call_id:e.id}),i?.(e.function.name,e.function.arguments,r)}continue}return{content:A(c),reasoning:d,usage:a,...o,...u}}},P=async function*(e,t,n=[],r){let{onToolHandled:i}=r??{};for(;;){let r=new Map,a=``,o=null,s,c=await e.post(`/chat/completions`,{stream:!0},{parser:async e=>e});for await(let e of S(c)){e.usage&&(s=e.usage);let t=e.choices?.[0];if(!t)continue;let{delta:n}=t,{content:i,tool_calls:c}=n,l=k(n);if(l&&(yield{reasoning:l}),i&&(a+=i,yield{content:i}),c)for(let e of c){let t=r.get(e.index)??{id:``,type:`function`,function:{name:``,arguments:``}};e.id&&(t.id=e.id),e.function?.name&&(t.function.name+=e.function.name),e.function?.arguments&&(t.function.arguments+=e.function.arguments),r.set(e.index,t)}t.finish_reason&&(o=t.finish_reason)}let l=Array.from(r.values());if(o===`tool_calls`&&l.length>0&&n.length>0){t.push({role:`assistant`,content:a,tool_calls:l});for(let e of l){let r=await O(e,n,{messages:t});t.push({role:`tool`,content:r,tool_call_id:e.id}),i?.(e.function.name,e.function.arguments,r)}continue}t.push({role:`assistant`,content:a}),s&&(yield{usage:s});break}};async function F(e,t,n){let{baseUrl:r,apiKey:i=``,model:a,customBody:o,inputs:s,...c}=e,{stream:l,tools:u=[],...d}=n??{},f=M(e,t);if(f)throw Error(`当前上下文含有模型不支持的输入类型:${f}`);let p=_(r,{headers:{Authorization:`Bearer ${i}`},body:{model:a??await T(r),messages:t,tools:u?.map(e=>D(e)[0]),...o}});return(l?P:N)(p,t,u,{...d,...c})}function I(e={},t=0){let{withTime:n=!0,withFileName:r=!0}=e;return(...e)=>{let i=[];if(n&&i.push(`[${new Date().toLocaleTimeString()}]`),r){let{stack:e}=Error(),n=(e?.split(`
7
+ `)[2+t]?.trim())?.match(/^at\s+(?:.*?\s*\()?(.*?):(\d+)(?::(\d+))?\)?/);if(n?.[1]){let e=n[1].split(/[/\\]/).pop();i.push(`[${e}:${n[2]}]`)}}i.push(...e),console.log(...i)}}function L(e,...n){if(t(e)&&(`withTime`in e||`withFileName`in e))return I(e,0);I({},1)(e,...n)}const R=e=>e?.role===`assistant`&&Array.isArray(e.tool_calls),z=(e,t)=>{for(t=Math.min(t,e.length),R(e[t-1])&&e[t]?.role!==`tool`&&t--;e[t]?.role===`tool`&&!R(e[t-1]);)t++;return t},B=(e,t)=>{let n=e.reduce((e,n)=>(n.role===`tool`&&(n.content=t(n.content),e++),e),0);n>0&&L(`软删除了${n}条工具调用结果消息`)},V=(e,t)=>{let n=[`image_url`,`input_audio`,`video_url`],r=e.reduce((e,r)=>(Array.isArray(r.content)&&r.content.some(e=>n.includes(e.type))&&(r.content=t(r.content),e++),e),0);r>0&&L(`软删除了${r}条旧图片/音频/视频消息`)},H=async(e,t)=>{let{model:n,keepPercent:r,systemPrompt:i}=t??{},a=e.findIndex(e=>e.role===`user`),o=Math.ceil(e.length*r),s=e.length-o;if(s<=a)return L(`消息太少,无需总结`),!1;s=z(e,s);let c=e.slice(a,s);c.push({role:`system`,content:i},{role:`user`,content:`开始总结上下文`});let[l,u]=await v(F(n,c));if(l)return L(`总结失败:${l.message}`),!1;e.splice(a,s-a,{role:`user`,content:`<summary>\n${u.content}\n</summary>`})},U=(e,t)=>{let n=e.findIndex(e=>e.role===`user`),r=Math.ceil(e.length*t),i=e.length-r;if(i<=n){L(`消息太少,无需硬删除`);return}i=z(e,i);let a=i-n;e.splice(n,a),L(`硬删除了${a}条较早的消息`)},W=async(e,t,n)=>{let{usage:r,ratioOfCompactToolResult:i=.6,replacerOfToolResultContent:a=()=>`已被消费`,ratioOfCompactMedia:o=.7,replacerOfMediaContent:s=()=>`已被丢弃`,ratioOfSummarize:c=.8,summarizeOptions:l}=n??{},u=t?.context??128e3,d=r?.total_tokens??E(e);if(d>u*i&&B(e,a),d>u*o&&V(e,s),d>u*c){let{keepPercent:n=.2,systemPrompt:r=`总结历史消息`}=l??{},[i]=await v(H(e,{model:t,keepPercent:n,systemPrompt:r}));if(!i)return;U(e,n)}},G=async(e,t)=>{let{maxRetries:n=5,shouldStop:r}=t??{},i;for(let t=0;t<n;t++)if(i=await e(t),r?.(i)===!0)return i;if(!r)return i;throw Error(`超过了最大循环次数(${n})且未满足停止执行条件`)},K=(e,t=-1)=>{let n=new Map,r=(...r)=>{let i=JSON.stringify(r),a=Date.now(),o=n.get(i);if(o&&a<o.expiresAt)return o.value;let s=t===-1?1/0:a+t*1e3,c=e.apply({setTtl:e=>{s=a+e*1e3}},r);if(c instanceof Promise){let e=c.then(e=>(n.set(i,{value:e,expiresAt:s}),e));return n.set(i,{value:e,expiresAt:s}),e}return n.set(i,{value:c,expiresAt:s}),c};return r.clear=()=>n.clear(),r.updateTtl=e=>{t=e;let r=Date.now(),i=r+e*1e3;for(let[e,t]of n.entries())t.expiresAt>r&&(t.expiresAt=i,n.set(e,t))},r},q=(e,t)=>Math.floor(Math.random()*(t-e+1))+e,J=(e,t=300)=>{let n=null;return(...r)=>{n&&clearTimeout(n),n=setTimeout(()=>{e(...r)},t)}};var Y=class{queue;constructor(){this.queue=Promise.resolve()}waitInQueue(){let e,t=new Promise(t=>{e=t}),n=this.queue.then(()=>e);return this.queue=t,n}};const X=async(e=150)=>new Promise(t=>{setTimeout(t,e)}),Z=(e,t=300)=>{let n=null;return function(...r){n||=setTimeout(()=>{n=null,e.apply(this,r)},t)}};export{Y as LockQueue,d as camelToSnake,f as capitalize,F as chatCompletions,W as compactMessages,m as compactStr,J as debounce,p as decapitalize,C as defineModel,w as defineTool,E as estimateTokens,h as extractErrorMessage,_ as fetcher,T as getModelName,y as getRealURL,x as imageUrlToBase64,e as isNil,t as isObject,n as isPrimitive,L as logger,G as loopUntil,r as mapKeys,i as mapValues,a as mergeObjects,o as omit,s as omitBy,S as parseSSE,c as pick,l as pickBy,g as qs,q as randomInt,X as sleep,u as snakeToCamel,Z as throttle,v as to,K as withCache};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nickyzj2023/utils",
3
- "version": "1.0.77",
3
+ "version": "1.0.79",
4
4
  "type": "module",
5
5
  "main": "dist/index.mjs",
6
6
  "module": "dist/index.mjs",