@huicai/cli 2026.711.0 → 2026.711.24617

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.
Files changed (49) hide show
  1. package/LICENSE +43 -21
  2. package/package.json +4 -3
  3. package/src/advisor.js +1 -46
  4. package/src/agent.js +1 -518
  5. package/src/api.js +1 -359
  6. package/src/autofix.js +1 -90
  7. package/src/banner.js +1 -83
  8. package/src/checkpoints.js +1 -99
  9. package/src/clipboard.js +1 -34
  10. package/src/codeindex.js +1 -229
  11. package/src/colors.js +1 -47
  12. package/src/commands.js +1 -1294
  13. package/src/compaction.js +1 -144
  14. package/src/config.js +1 -86
  15. package/src/consult.js +1 -65
  16. package/src/context-report.js +1 -119
  17. package/src/context.js +1 -152
  18. package/src/cron.js +1 -64
  19. package/src/daemon.js +1 -62
  20. package/src/device.js +1 -88
  21. package/src/diffview.js +1 -115
  22. package/src/doctor.js +1 -72
  23. package/src/executors.js +1 -391
  24. package/src/forks.js +1 -59
  25. package/src/gitflow.js +1 -140
  26. package/src/hooks.js +1 -207
  27. package/src/ide.js +1 -53
  28. package/src/input.js +1 -182
  29. package/src/insights.js +1 -102
  30. package/src/kbsync.js +1 -141
  31. package/src/markdown.js +0 -0
  32. package/src/markup-filter.js +1 -85
  33. package/src/mcp.js +1 -207
  34. package/src/memory.js +1 -161
  35. package/src/permissions.js +1 -120
  36. package/src/render.js +1 -138
  37. package/src/repl.js +1 -271
  38. package/src/scan.js +1 -84
  39. package/src/schedule.js +1 -48
  40. package/src/session.js +1 -140
  41. package/src/settings.js +0 -0
  42. package/src/skills.js +1 -248
  43. package/src/subagent.js +1 -183
  44. package/src/todo.js +1 -75
  45. package/src/tools.js +1 -147
  46. package/src/transcript.js +1 -94
  47. package/src/tui/app.js +1 -666
  48. package/src/usage.js +1 -43
  49. package/src/watch.js +1 -74
package/src/todo.js CHANGED
@@ -1,75 +1 @@
1
- // 任务清单工具:
2
- // 模型用 todo_write 维护当前任务的步骤清单(每次提交完整列表),CLI 实时渲染进度。
3
- // 纯客户端状态(不落盘、不进模型上下文之外的系统),状态由调用方(会话)持有以跨轮保留。
4
-
5
- const STATUSES = new Set(['pending', 'in_progress', 'completed']);
6
-
7
- export const TODO_TOOL_NAME = 'todo_write';
8
-
9
- /**
10
- * 构造 todo_write 工具。
11
- * @param {{state:{list:any[]}, emit:(list:any[])=>void}} p state 由调用方持有(跨轮保留);emit 用于 UI 渲染
12
- * @returns {{spec:object, run:Function, readOnly:boolean}}
13
- */
14
- export function makeTodoTool({ state, emit }) {
15
- const spec = {
16
- type: 'function',
17
- function: {
18
- name: TODO_TOOL_NAME,
19
- description:
20
- '维护当前任务的步骤清单,帮助用户看到进度。多步骤/非琐碎任务开始前先列出计划,'
21
- + '每完成一步就更新状态(同一时刻至多一项 in_progress)。每次调用需提交完整清单(全量覆盖)。'
22
- + '单步小事无需使用。',
23
- parameters: {
24
- type: 'object',
25
- properties: {
26
- todos: {
27
- type: 'array',
28
- description: '完整的任务清单(全量覆盖旧清单)',
29
- items: {
30
- type: 'object',
31
- properties: {
32
- content: { type: 'string', description: '任务内容(简短祈使句)' },
33
- status: { type: 'string', enum: ['pending', 'in_progress', 'completed'], description: '任务状态' },
34
- },
35
- required: ['content', 'status'],
36
- },
37
- },
38
- },
39
- required: ['todos'],
40
- },
41
- },
42
- };
43
-
44
- const run = async (args) => {
45
- const raw = Array.isArray(args?.todos) ? args.todos : null;
46
- if (!raw) return 'todo_write 需要 todos 数组';
47
- const list = raw
48
- .map((t) => ({
49
- content: String(t?.content ?? '').trim(),
50
- status: STATUSES.has(t?.status) ? t.status : 'pending',
51
- }))
52
- .filter((t) => t.content)
53
- .slice(0, 50);
54
- state.list = list;
55
- try {
56
- emit?.(list);
57
- } catch {
58
- /* 渲染失败不影响流程 */
59
- }
60
- const done = list.filter((t) => t.status === 'completed').length;
61
- return `已更新任务清单:共 ${list.length} 项,完成 ${done} 项。`;
62
- };
63
-
64
- return { readOnly: true, spec, run };
65
- }
66
-
67
- /** 把清单渲染为多行文本(REPL/降级输出用)。 */
68
- export function formatTodos(list) {
69
- return (list || [])
70
- .map((t) => {
71
- const mark = t.status === 'completed' ? '☑' : t.status === 'in_progress' ? '◐' : '☐';
72
- return `${mark} ${t.content}`;
73
- })
74
- .join('\n');
75
- }
1
+ const t=new Set(["pending","in_progress","completed"]);export const TODO_TOOL_NAME="todo_write";export function makeTodoTool({state:e,emit:o}){return{readOnly:!0,spec:{type:"function",function:{name:"todo_write",description:"维护当前任务的步骤清单,帮助用户看到进度。多步骤/非琐碎任务开始前先列出计划,每完成一步就更新状态(同一时刻至多一项 in_progress)。每次调用需提交完整清单(全量覆盖)。单步小事无需使用。",parameters:{type:"object",properties:{todos:{type:"array",description:"完整的任务清单(全量覆盖旧清单)",items:{type:"object",properties:{content:{type:"string",description:"任务内容(简短祈使句)"},status:{type:"string",enum:["pending","in_progress","completed"],description:"任务状态"}},required:["content","status"]}}},required:["todos"]}}},run:async n=>{const r=Array.isArray(n?.todos)?n.todos:null;if(!r)return"todo_write 需要 todos 数组";const s=r.map(e=>({content:String(e?.content??"").trim(),status:t.has(e?.status)?e.status:"pending"})).filter(t=>t.content).slice(0,50);e.list=s;try{o?.(s)}catch{}const i=s.filter(t=>"completed"===t.status).length;return`已更新任务清单:共 ${s.length} 项,完成 ${i} 项。`}}}export function formatTodos(t){return(t||[]).map(t=>`${"completed"===t.status?"☑":"in_progress"===t.status?"◐":"☐"} ${t.content}`).join("\n")}
package/src/tools.js CHANGED
@@ -1,147 +1 @@
1
- // 本机工具的 OpenAI 函数规格(随请求透传给平台模型)+ 名称→执行器分发。
2
-
3
- import * as ex from './executors.js';
4
- import { searchCode } from './codeindex.js';
5
-
6
- const obj = (properties, required) => ({ type: 'object', properties, required: required || [] });
7
- const str = (description) => ({ type: 'string', description });
8
- const int = (description) => ({ type: 'integer', description });
9
- const bool = (description) => ({ type: 'boolean', description });
10
-
11
- /** @type {any[]} 暴露给模型的工具(函数调用规格)。 */
12
- export const TOOL_SPECS = [
13
- {
14
- type: 'function',
15
- function: {
16
- name: 'read_file',
17
- description:
18
- '读取文本文件内容,每行带「行号→」前缀(行号仅供定位,不属于文件内容,编辑时不要包含它)。'
19
- + '默认最多 2000 行,可用 offset(起始行,1基)/limit(行数) 分段读取大文件。',
20
- parameters: obj(
21
- { path: str('相对项目根的文件路径'), offset: int('可选:起始行(1基)'), limit: int('可选:读取行数') },
22
- ['path'],
23
- ),
24
- },
25
- },
26
- {
27
- type: 'function',
28
- function: {
29
- name: 'list_files',
30
- description: '列出目录下的文件树(跳过 .git/node_modules 等)。可选 path 指定子目录(默认项目根)。',
31
- parameters: obj({ path: str('可选:子目录路径,默认当前项目根') }, []),
32
- },
33
- },
34
- {
35
- type: 'function',
36
- function: {
37
- name: 'glob',
38
- description: '按 glob 模式查找文件(支持 ** 与 *),如 "**/*.ts"、"src/**/*.java"。',
39
- parameters: obj({ pattern: str('glob 模式') }, ['pattern']),
40
- },
41
- },
42
- {
43
- type: 'function',
44
- function: {
45
- name: 'search_code',
46
- description:
47
- '按「概念/功能」检索最相关的代码块(BM25 相关性排序,比 grep 更懂「哪段代码在做这件事」)。'
48
- + '适合「登录逻辑在哪」「哪里处理支付回调」这类找相关实现;要找精确字符串仍用 grep。返回 文件:行范围 + 代码块。',
49
- parameters: obj(
50
- { query: str('要找的功能/概念(自然语言或关键词组合,多写几个相关词更准)'), k: int('可选:返回块数,默认 8,上限 20') },
51
- ['query'],
52
- ),
53
- },
54
- },
55
- {
56
- type: 'function',
57
- function: {
58
- name: 'grep',
59
- description:
60
- '在文件内容中按正则搜索,返回 匹配文件:行号: 该行。可选 path 限定目录、glob 限定文件名模式、'
61
- + 'ignore_case 忽略大小写、files_only 只列出命中文件。',
62
- parameters: obj(
63
- {
64
- pattern: str('正则表达式'),
65
- path: str('可选:搜索目录,默认项目根'),
66
- glob: str('可选:文件名 glob 过滤'),
67
- ignore_case: bool('可选:忽略大小写(默认否)'),
68
- files_only: bool('可选:只返回命中文件列表,不含匹配行(默认否)'),
69
- max_results: int('可选:最多返回条数(默认 200,上限 500)'),
70
- },
71
- ['pattern'],
72
- ),
73
- },
74
- },
75
- {
76
- type: 'function',
77
- function: {
78
- name: 'write_file',
79
- description: '把文本写入文件(覆盖式,自动建父目录)。仅限当前项目目录内。仅用于新建文件或全量重写;小改动优先 edit_file。',
80
- parameters: obj({ path: str('相对项目根的文件路径'), content: str('完整文件内容') }, ['path', 'content']),
81
- },
82
- },
83
- {
84
- type: 'function',
85
- function: {
86
- name: 'edit_file',
87
- description:
88
- '精确替换文件中的片段:old_string 必须是文件原文(不含 read_file 的行号前缀)且在文件中唯一'
89
- + '(除非 replace_all=true)。仅限项目目录内。',
90
- parameters: obj(
91
- {
92
- path: str('相对项目根的文件路径'),
93
- old_string: str('被替换的原文(需在文件中唯一,除非 replace_all;不要包含行号前缀)'),
94
- new_string: str('替换为的新内容'),
95
- replace_all: bool('是否替换全部匹配(默认否)'),
96
- },
97
- ['path', 'old_string', 'new_string'],
98
- ),
99
- },
100
- },
101
- {
102
- type: 'function',
103
- function: {
104
- name: 'run_bash',
105
- description:
106
- '在项目目录下执行一条 shell 命令并返回输出(stdout+stderr+退出码)。用于构建、测试、git、脚手架等。'
107
- + '默认超时 120s(可用 timeout 调整)。避免用它读文件/搜索(用 read_file/grep 更高效)。',
108
- parameters: obj({ command: str('要执行的 shell 命令'), timeout: int('可选:超时毫秒') }, ['command']),
109
- },
110
- },
111
- {
112
- type: 'function',
113
- function: {
114
- name: 'fetch_url',
115
- description:
116
- '抓取一个 http/https 网页或接口(GET),HTML 自动转为纯文本。用于查在线文档、API 说明、Issue 等。'
117
- + '结果可能很长,尽量带着明确目的使用。',
118
- parameters: obj({ url: str('完整 URL(http/https)'), timeout: int('可选:超时毫秒,默认 30000') }, ['url']),
119
- },
120
- },
121
- ];
122
-
123
- /** 只读工具(默认自动放行,无需审批)。fetch_url 涉及外发请求,需用户授权。 */
124
- export const READONLY_TOOLS = new Set(['read_file', 'list_files', 'glob', 'grep', 'search_code']);
125
-
126
- const EXECUTORS = {
127
- read_file: ex.readFile,
128
- list_files: ex.listFiles,
129
- glob: ex.glob,
130
- grep: ex.grep,
131
- search_code: searchCode,
132
- write_file: ex.writeFile,
133
- edit_file: ex.editFile,
134
- run_bash: ex.runBash,
135
- fetch_url: ex.fetchUrl,
136
- };
137
-
138
- /** 执行一个本机工具,返回文本结果(回填给模型)。config 传入供个别工具用(如 search_code 的向量重排)。抛错转为可读文本,保证循环不中断。 */
139
- export async function runTool(name, args, cwd, config) {
140
- const fn = EXECUTORS[name];
141
- if (!fn) return `未知工具:${name}`;
142
- try {
143
- return await fn(cwd, args || {}, config);
144
- } catch (e) {
145
- return `工具执行失败:${e.message}`;
146
- }
147
- }
1
+ import*as e from"./executors.js";import{searchCode as t}from"./codeindex.js";const r=(e,t)=>({type:"object",properties:e,required:t||[]}),n=e=>({type:"string",description:e}),i=e=>({type:"integer",description:e}),o=e=>({type:"boolean",description:e});export const TOOL_SPECS=[{type:"function",function:{name:"read_file",description:"读取文本文件内容,每行带「行号→」前缀(行号仅供定位,不属于文件内容,编辑时不要包含它)。默认最多 2000 行,可用 offset(起始行,1基)/limit(行数) 分段读取大文件。",parameters:r({path:n("相对项目根的文件路径"),offset:i("可选:起始行(1基)"),limit:i("可选:读取行数")},["path"])}},{type:"function",function:{name:"list_files",description:"列出目录下的文件树(跳过 .git/node_modules 等)。可选 path 指定子目录(默认项目根)。",parameters:r({path:n("可选:子目录路径,默认当前项目根")},[])}},{type:"function",function:{name:"glob",description:'按 glob 模式查找文件(支持 ** 与 *),如 "**/*.ts"、"src/**/*.java"。',parameters:r({pattern:n("glob 模式")},["pattern"])}},{type:"function",function:{name:"search_code",description:"按「概念/功能」检索最相关的代码块(BM25 相关性排序,比 grep 更懂「哪段代码在做这件事」)。适合「登录逻辑在哪」「哪里处理支付回调」这类找相关实现;要找精确字符串仍用 grep。返回 文件:行范围 + 代码块。",parameters:r({query:n("要找的功能/概念(自然语言或关键词组合,多写几个相关词更准)"),k:i("可选:返回块数,默认 8,上限 20")},["query"])}},{type:"function",function:{name:"grep",description:"在文件内容中按正则搜索,返回 匹配文件:行号: 该行。可选 path 限定目录、glob 限定文件名模式、ignore_case 忽略大小写、files_only 只列出命中文件。",parameters:r({pattern:n("正则表达式"),path:n("可选:搜索目录,默认项目根"),glob:n("可选:文件名 glob 过滤"),ignore_case:o("可选:忽略大小写(默认否)"),files_only:o("可选:只返回命中文件列表,不含匹配行(默认否)"),max_results:i("可选:最多返回条数(默认 200,上限 500)")},["pattern"])}},{type:"function",function:{name:"write_file",description:"把文本写入文件(覆盖式,自动建父目录)。仅限当前项目目录内。仅用于新建文件或全量重写;小改动优先 edit_file。",parameters:r({path:n("相对项目根的文件路径"),content:n("完整文件内容")},["path","content"])}},{type:"function",function:{name:"edit_file",description:"精确替换文件中的片段:old_string 必须是文件原文(不含 read_file 的行号前缀)且在文件中唯一(除非 replace_all=true)。仅限项目目录内。",parameters:r({path:n("相对项目根的文件路径"),old_string:n("被替换的原文(需在文件中唯一,除非 replace_all;不要包含行号前缀)"),new_string:n("替换为的新内容"),replace_all:o("是否替换全部匹配(默认否)")},["path","old_string","new_string"])}},{type:"function",function:{name:"run_bash",description:"在项目目录下执行一条 shell 命令并返回输出(stdout+stderr+退出码)。用于构建、测试、git、脚手架等。默认超时 120s(可用 timeout 调整)。避免用它读文件/搜索(用 read_file/grep 更高效)。",parameters:r({command:n("要执行的 shell 命令"),timeout:i("可选:超时毫秒")},["command"])}},{type:"function",function:{name:"fetch_url",description:"抓取一个 http/https 网页或接口(GET),HTML 自动转为纯文本。用于查在线文档、API 说明、Issue 等。结果可能很长,尽量带着明确目的使用。",parameters:r({url:n("完整 URL(http/https)"),timeout:i("可选:超时毫秒,默认 30000")},["url"])}}];export const READONLY_TOOLS=new Set(["read_file","list_files","glob","grep","search_code"]);const s={read_file:e.readFile,list_files:e.listFiles,glob:e.glob,grep:e.grep,search_code:t,write_file:e.writeFile,edit_file:e.editFile,run_bash:e.runBash,fetch_url:e.fetchUrl};export async function runTool(e,t,r,n){const i=s[e];if(!i)return`未知工具:${e}`;try{return await i(r,t||{},n)}catch(e){return`工具执行失败:${e.message}`}}
package/src/transcript.js CHANGED
@@ -1,94 +1 @@
1
- // 对话转录:导出为 Markdown(/export)与一句话小结(/recap)。
2
- // recap 直连原始模型(伪模型会触发平台编排+计费,故回退 DEFAULT_MODEL),不写入对话。
3
-
4
- import { streamChat } from './api.js';
5
- import { DEFAULT_MODEL } from './config.js';
6
-
7
- const PSEUDO = /^(agent|workflow|datahuman)-/;
8
-
9
- /** 提取消息文本(字符串或多模态数组)。 */
10
- export function textOf(content) {
11
- if (typeof content === 'string') return content;
12
- if (Array.isArray(content)) {
13
- return content.map((p) => (p?.type === 'text' ? p.text : p?.type === 'image_url' ? '[图片]' : '')).join('\n');
14
- }
15
- return '';
16
- }
17
-
18
- /** 会话 → Markdown 文本(跳过 system;工具调用/结果折叠展示)。 */
19
- export function toMarkdown(session) {
20
- const s = session || {};
21
- const lines = [
22
- '# HuiCai 对话记录',
23
- '',
24
- `- 会话:${s.id || ''}`,
25
- `- 模型:${s.model || ''}`,
26
- `- 导出时间:${new Date().toLocaleString()}`,
27
- '',
28
- '---',
29
- '',
30
- ];
31
- for (const m of s.messages || []) {
32
- if (m.role === 'system') continue;
33
- if (m.role === 'user') {
34
- lines.push('### 🧑 用户', '', textOf(m.content).trim() || '(空)', '');
35
- } else if (m.role === 'assistant') {
36
- const text = textOf(m.content).trim();
37
- if (text) lines.push('### 🤖 助手', '', text, '');
38
- for (const tc of m.tool_calls || []) {
39
- const argsStr = String(tc.function?.arguments || '').replace(/\s+/g, ' ').slice(0, 200);
40
- lines.push(`> 🔧 \`${tc.function?.name}(${argsStr})\``, '');
41
- }
42
- } else if (m.role === 'tool') {
43
- const out = String(m.content || '').slice(0, 1500);
44
- lines.push('> 工具结果:', '', '```', out, '```', '');
45
- }
46
- }
47
- return lines.join('\n');
48
- }
49
-
50
- /** 从对话提炼「值得跨会话长期记住」的项目知识(直连原始模型)。返回事实数组(可能为空)。 */
51
- export async function distillMemory(messages, config, existingMemory = '') {
52
- const model = PSEUDO.test(String(config.model || '')) ? (config.compactModel || DEFAULT_MODEL) : config.model;
53
- const transcript = (messages || [])
54
- .filter((m) => m.role !== 'system')
55
- .map((m) => `[${m.role}] ${textOf(m.content).replace(/\s+/g, ' ').slice(0, 600)}`)
56
- .join('\n')
57
- .slice(0, 16000);
58
- if (!transcript.trim()) return [];
59
- const sys = '你是项目知识提炼器。从下面这段对话/执行记录中,提炼出**值得跨会话长期记住**的稳定事实/约定/结论/踩坑'
60
- + '(如构建与测试命令、架构决定、代码约定、易错点、外部依赖要点)。要求:每条一行,以「- 」开头,简洁具体;'
61
- + '只输出真正长期有用的,不要一次性细节、不要大段代码;若与「已有记忆」重复则跳过;最多 5 条;没有则输出「(无)」。';
62
- const user = (existingMemory ? '【已有记忆(勿重复)】\n' + existingMemory + '\n\n' : '') + '【本次记录】\n' + transcript;
63
- const r = await streamChat({
64
- baseUrl: config.baseUrl, apiKey: config.apiKey, model,
65
- messages: [{ role: 'system', content: sys }, { role: 'user', content: user }],
66
- });
67
- const text = (r.content || '').trim();
68
- if (!text || /^[((]无[))]$/.test(text)) return [];
69
- return text.split('\n')
70
- .map((l) => l.replace(/^[-*]\s*/, '').trim())
71
- .filter((l) => l && !/^[((]无[))]$/.test(l))
72
- .slice(0, 5);
73
- }
74
-
75
- /** 一句话小结当前对话(直连原始模型;不落对话)。返回一行文本。 */
76
- export async function recapConversation(messages, config) {
77
- const model = PSEUDO.test(String(config.model || '')) ? (config.compactModel || DEFAULT_MODEL) : config.model;
78
- const transcript = (messages || [])
79
- .filter((m) => m.role !== 'system')
80
- .map((m) => `[${m.role}] ${textOf(m.content).replace(/\s+/g, ' ').slice(0, 500)}`)
81
- .join('\n')
82
- .slice(0, 12000);
83
- if (!transcript.trim()) return '(对话尚无内容)';
84
- const r = await streamChat({
85
- baseUrl: config.baseUrl,
86
- apiKey: config.apiKey,
87
- model,
88
- messages: [
89
- { role: 'system', content: '用一句话(不超过40个汉字)概括这段对话到目前为止在做什么、进展如何。只输出这一句话,不要引号、不要多余说明。' },
90
- { role: 'user', content: transcript },
91
- ],
92
- });
93
- return (r.content || '').trim().split('\n')[0] || '(无法生成小结)';
94
- }
1
+ import{streamChat as t}from"./api.js";import{DEFAULT_MODEL as e}from"./config.js";const n=/^(agent|workflow|datahuman)-/;export function textOf(t){return"string"==typeof t?t:Array.isArray(t)?t.map(t=>"text"===t?.type?t.text:"image_url"===t?.type?"[图片]":"").join("\n"):""}export function toMarkdown(t){const e=t||{},n=["# HuiCai 对话记录","",`- 会话:${e.id||""}`,`- 模型:${e.model||""}`,`- 导出时间:${(new Date).toLocaleString()}`,"","---",""];for(const t of e.messages||[])if("system"!==t.role)if("user"===t.role)n.push("### 🧑 用户","",textOf(t.content).trim()||"(空)","");else if("assistant"===t.role){const e=textOf(t.content).trim();e&&n.push("### 🤖 助手","",e,"");for(const e of t.tool_calls||[]){const t=String(e.function?.arguments||"").replace(/\s+/g," ").slice(0,200);n.push(`> 🔧 \`${e.function?.name}(${t})\``,"")}}else if("tool"===t.role){const e=String(t.content||"").slice(0,1500);n.push("> 工具结果:","","```",e,"```","")}return n.join("\n")}export async function distillMemory(o,r,s=""){const i=n.test(String(r.model||""))?r.compactModel||e:r.model,l=(o||[]).filter(t=>"system"!==t.role).map(t=>`[${t.role}] ${textOf(t.content).replace(/\s+/g," ").slice(0,600)}`).join("\n").slice(0,16e3);if(!l.trim())return[];const c=(s?"【已有记忆(勿重复)】\n"+s+"\n\n":"")+"【本次记录】\n"+l,a=((await t({baseUrl:r.baseUrl,apiKey:r.apiKey,model:i,messages:[{role:"system",content:"你是项目知识提炼器。从下面这段对话/执行记录中,提炼出**值得跨会话长期记住**的稳定事实/约定/结论/踩坑(如构建与测试命令、架构决定、代码约定、易错点、外部依赖要点)。要求:每条一行,以「- 」开头,简洁具体;只输出真正长期有用的,不要一次性细节、不要大段代码;若与「已有记忆」重复则跳过;最多 5 条;没有则输出「(无)」。"},{role:"user",content:c}]})).content||"").trim();return!a||/^[((]无[))]$/.test(a)?[]:a.split("\n").map(t=>t.replace(/^[-*]\s*/,"").trim()).filter(t=>t&&!/^[((]无[))]$/.test(t)).slice(0,5)}export async function recapConversation(o,r){const s=n.test(String(r.model||""))?r.compactModel||e:r.model,i=(o||[]).filter(t=>"system"!==t.role).map(t=>`[${t.role}] ${textOf(t.content).replace(/\s+/g," ").slice(0,500)}`).join("\n").slice(0,12e3);if(!i.trim())return"(对话尚无内容)";return((await t({baseUrl:r.baseUrl,apiKey:r.apiKey,model:s,messages:[{role:"system",content:"用一句话(不超过40个汉字)概括这段对话到目前为止在做什么、进展如何。只输出这一句话,不要引号、不要多余说明。"},{role:"user",content:i}]})).content||"").trim().split("\n")[0]||"(无法生成小结)"}