@nickyzj2023/utils 1.0.76 → 1.0.78

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
@@ -1,3 +1,5 @@
1
+ # utils
2
+
1
3
  男生自用全新前端工具库,0依赖
2
4
 
3
5
  ## 安装到你的项目里
@@ -21,9 +23,11 @@ import { fetcher, to } from "@nickyzj2023/utils";
21
23
 
22
24
  const api = fetcher("https://api.example.com");
23
25
 
24
- const [error, data] = await to(api.get<Blog>("/blogs/hello-world"));
26
+ const [error, data] = await to(
27
+ api.get<Blog>("/blogs/hello-world")
28
+ );
25
29
  if (error) {
26
- console.error(error);
30
+ console.log(error);
27
31
  return;
28
32
  }
29
33
 
package/dist/index.d.mts CHANGED
@@ -4,10 +4,19 @@ declare namespace AI {
4
4
  baseUrl: string; /** 不传则自动使用{baseUrl}/models接口的第一个模型 */
5
5
  model?: string;
6
6
  apiKey?: string; /** POST /chat/completions时注入自定义请求体 */
7
- customBody?: Record<string, any>; /** 模型支持的消息输入类型,如果填了,则会在调用chatCompletions前校验上下文,含有不支持的输入时抑制请求 */
8
- inputs?: ["text" | "image" | "video" | "audio" | "file"]; /** 模型的最大上下文(输入+输出),如果填了,则会在调用chatCompletions后选择性调用compact(当前上下文>最大上下文*80%时) */
7
+ customBody?: Record<string, any>;
8
+ /**
9
+ * 模型支持的消息输入类型
10
+ * @default ["text"]
11
+ * @remarks 会在调用chatCompletions前校验上下文,含有不支持的输入时抑制请求 */
12
+ inputs?: InputType[];
13
+ /**
14
+ * 模型的最大上下文(输入+输出)
15
+ * @default 128000
16
+ * @remarks 会在调用chatCompletions后智能调用compact(如当前上下文>最大上下文*80%时),详见chatCompletions.autoCompact配置 */
9
17
  context?: number;
10
18
  };
19
+ type InputType = "text" | "image" | "video" | "audio" | "file";
11
20
  type Message = {
12
21
  role: "system" | "user" | "assistant" | "tool" | "function"; /** OpenRouter的思考内容字段,其他供应商的会尽可能合并到该字段内 */
13
22
  reasoning?: string | null;
@@ -70,6 +79,13 @@ declare namespace AI {
70
79
  //#endregion
71
80
  //#region src/ai/chatCompletions/types.d.ts
72
81
  declare namespace ChatCompletions {
82
+ /** chatCompletions的第三个参数 */
83
+ type Options = {
84
+ stream?: boolean;
85
+ tools?: AI.ToolDefinition[]; /** 工具运行结束后(无论成功失败)的回调,可用于打印日志 */
86
+ onToolHandled?: (name: string, args: string, result: any) => void; /** 自动压缩上下文 */
87
+ autoCompact?: {};
88
+ };
73
89
  /** 非流式POST /chat/completions的响应结果 */
74
90
  type NonStreamResponse = {
75
91
  id: string;
@@ -122,14 +138,42 @@ declare namespace ChatCompletions {
122
138
  //#endregion
123
139
  //#region src/ai/chatCompletions/index.d.ts
124
140
  /**
125
- * 兼容OpenAI API的聊天补全函数
141
+ * 兼容OpenAI API的聊天补全函数(流式模式)
126
142
  * - 自动处理工具调用
127
- * - 支持普通/流式响应
143
+ * - 传入 `stream: true` 时返回异步迭代器,逐块产出内容与 usage
128
144
  *
129
145
  * @param model 模型配置,包含model、baseUrl、apiKey
130
146
  * @param messages OpenAI API兼容的消息数组
131
- * @param extraBody 可选的额外参数,如tools、temperature、stream
132
- * @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, ... }` 完整结果
133
177
  *
134
178
  * @example
135
179
  * // 最简调用
@@ -140,12 +184,12 @@ declare namespace ChatCompletions {
140
184
  * );
141
185
  * console.log(reasoning); // "The user said..."
142
186
  * console.log(content); // "你好!有什么我可以帮你的吗?"
143
- * 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] }
144
188
  *
145
189
  * @example
146
190
  * // 工具调用
147
191
  * const { content, usage } = await chatCompletions(
148
- * { 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" },
149
193
  * [{ role: "user", content: "查询上海天气" }],
150
194
  * {
151
195
  * tools: [{
@@ -159,30 +203,58 @@ declare namespace ChatCompletions {
159
203
  * }],
160
204
  * },
161
205
  * );
162
- *
163
- * @example
164
- * // 流式传输
165
- * const result = await chatCompletions(
166
- * { baseUrl: "http://127.0.0.1:11434/v1" },
167
- * [{ role: "user", content: "你好" }],
168
- * { stream: true },
169
- * );
170
- * for await (const { content, usage } of result) {
171
- * if (content) {
172
- * console.log("流式传输中:", content);
173
- * } else if (usage) {
174
- * console.log("对话结束,消耗:", usage);
175
- * }
176
- * }
177
206
  */
178
- declare function chatCompletions(model: AI.Model, messages: AI.Message[], options: {
179
- stream: true;
180
- tools?: AI.ToolDefinition[];
181
- }): Promise<AsyncGenerator<ChatCompletions.StreamChunk>>;
182
- declare function chatCompletions(model: AI.Model, messages: AI.Message[], options?: {
183
- stream?: boolean;
184
- tools?: AI.ToolDefinition[];
185
- }): Promise<ChatCompletions.NonStreamResult>;
207
+ declare function chatCompletions(model: AI.Model, messages: AI.Message[], options?: ChatCompletions.Options): Promise<ChatCompletions.NonStreamResult>;
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 compact: (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
+ ratioToCompactMedia?: 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
+ ratioToSummarize?: number;
252
+ /**
253
+ * 总结消息时的配置项
254
+ * @default { keepPercent: 0.2, model: undefined, systemPrompt: "总结历史消息" }
255
+ */
256
+ summarizeOptions?: Compact.SummarizeOptions;
257
+ }) => Promise<void>;
186
258
  //#endregion
187
259
  //#region src/ai/helper.d.ts
188
260
  /**
@@ -199,6 +271,13 @@ declare const defineTool: (name: AI.ToolDefinition["function"]["name"], descript
199
271
  * 从GET /models获取模型名称
200
272
  */
201
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;
202
281
  //#endregion
203
282
  //#region src/dom/logger.d.ts
204
283
  /**
@@ -219,18 +298,11 @@ interface LoggerOptions {
219
298
  /**
220
299
  * 带额外信息的 console.log
221
300
  *
222
- * **直接调用**:使用默认选项打印消息
223
- * @param messages - 日志消息,支持多条
224
- *
225
301
  * **预配置**:先传入选项返回 logger 函数,再调用打印
226
302
  * @param options - 配置选项
227
303
  * @returns 配置后的 logger 函数
228
304
  *
229
305
  * @example
230
- * // 直接调用(默认显示时间和文件名)
231
- * logger("调试信息"); // "[14:30:00] [index.ts:15:2] 调试信息"
232
- * logger("消息1", "消息2"); // "[14:30:00] [index.ts:15:2] 消息1 消息2"
233
- *
234
306
  * // 预配置后调用
235
307
  * const myLogger = logger({ withTime: true, withFileName: true });
236
308
  * myLogger("一段消息", "另一段消息"); // "[18:59:47] [index.ts:137:2] 一段消息 另一段消息"
@@ -240,6 +312,17 @@ interface LoggerOptions {
240
312
  * plainLogger("纯文件名前缀"); // "[index.ts:15:2] 纯文件名前缀"
241
313
  */
242
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
+ */
243
326
  declare function logger(...messages: any[]): void;
244
327
  //#endregion
245
328
  //#region src/function/loop-until.d.ts
@@ -830,4 +913,4 @@ declare const sleep: (time?: number) => Promise<unknown>;
830
913
  */
831
914
  declare const throttle: <T extends (...args: any[]) => any>(fn: T, delay?: number) => (this: any, ...args: Parameters<T>) => void;
832
915
  //#endregion
833
- 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, compact, 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=>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=async(e,t,n=[])=>{for(;;){let{choices:r,usage:i,...a}=await e.post(`/chat/completions`,{}),{message:o}=r?.[0]??{};if(!o)throw Error(`模型没有回复任何内容`);t.push(o);let{content:s=``,tool_calls:c=[],...l}=o,u=O(o);if(c.length>0&&n.length>0){for(let e of c){let r=await D(e,n,{messages:t});t.push({role:`tool`,content:r,tool_call_id:e.id})}continue}return{content:k(s),reasoning:u,usage:i,...a,...l}}},j=async function*(e,t,n=[]){for(;;){let r=new Map,i=``,a=null,o,s=await e.post(`/chat/completions`,{stream:!0},{parser:async e=>e});for await(let e of S(s)){e.usage&&(o=e.usage);let t=e.choices?.[0];if(!t)continue;let{delta:n}=t,{content:s,tool_calls:c}=n,l=O(n);if(l&&(yield{reasoning:l}),s&&(i+=s,yield{content:s}),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&&(a=t.finish_reason)}let c=Array.from(r.values());if(a===`tool_calls`&&c.length>0&&n.length>0){t.push({role:`assistant`,content:i,tool_calls:c});for(let e of c){let r=await D(e,n,{messages:t});t.push({role:`tool`,content:r,tool_call_id:e.id})}continue}t.push({role:`assistant`,content:i}),o&&(yield{usage:o});break}};async function M(e,t,n){let{baseUrl:r,apiKey:i=``,model:a,...o}=e,{stream:s,tools:c=[]}=n??{},l=_(r,{headers:{Authorization:`Bearer ${i}`},body:{model:a??await T(r),messages:t,tools:c?.map(e=>E(e)[0]),...o.customBody}});return(s?j:A)(l,t,c)}function N(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 P(e,...n){if(t(e)&&(`withTime`in e||`withFileName`in e))return N(e,0);N({},1)(e,...n)}const F=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})且未满足停止执行条件`)},I=(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},L=(e,t)=>Math.floor(Math.random()*(t-e+1))+e,R=(e,t=300)=>{let n=null;return(...r)=>{n&&clearTimeout(n),n=setTimeout(()=>{e(...r)},t)}};var z=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 B=async(e=150)=>new Promise(t=>{setTimeout(t,e)}),V=(e,t=300)=>{let n=null;return function(...r){n||=setTimeout(()=>{n=null,e.apply(this,r)},t)}};export{z as LockQueue,d as camelToSnake,f as capitalize,M as chatCompletions,m as compactStr,R 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,P as logger,F 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,L as randomInt,B as sleep,u as snakeToCamel,V as throttle,v as to,I 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=()=>`已被消费`,ratioToCompactMedia:o=.7,replacerOfMediaContent:s=()=>`已被丢弃`,ratioToSummarize: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 compact,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.76",
3
+ "version": "1.0.78",
4
4
  "type": "module",
5
5
  "main": "dist/index.mjs",
6
6
  "module": "dist/index.mjs",