zhitalk 0.0.8 → 0.0.10
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 +12 -4
- package/dist/agent/agent.js +43 -24
- package/dist/agent/cli.js +56 -4
- package/dist/agent/config.js +4 -1
- package/dist/agent/model.js +9 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,22 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
[智语 Zhitalk](https://zhitalk.chat/) 是一个 AI Agent 个人助手,像 OpenClaw 小龙虾一样。 包含 tools skills memory hook subagent MCP-server 等 Agent 功能。你可以和它聊天,给它分配任务,让它操作文件...
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
📖 你可以免费围观项目进展,也可以加入学习 [跟双越老师从 0 开发一个 AI Agent 智能体](https://www.huashuiai.com/pub/ai-agent-camp-new)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Usage
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
使用 npm 全局安装。注意,Windows 用户使用管理员打开 cmd 再执行。
|
|
10
10
|
|
|
11
11
|
```
|
|
12
12
|
npm install zhitalk -g
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
查看版本
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
zhitalk --version
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
启动项目
|
|
16
22
|
|
|
17
23
|
```
|
|
18
24
|
zhitalk
|
|
19
25
|
```
|
|
20
26
|
|
|
27
|
+
第一次执行时,需要你修改配置文件,参考 https://zhitalk.chat/#config
|
|
28
|
+
|
|
21
29
|
## Dev
|
|
22
30
|
|
|
23
31
|
### Tech
|
package/dist/agent/agent.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SqliteSaver } from '@langchain/langgraph-checkpoint-sqlite';
|
|
2
2
|
import { SystemMessage, AIMessage, HumanMessage, ToolMessage, } from '@langchain/core/messages';
|
|
3
|
-
import { StateGraph, Annotation, START, END, interrupt, Command, } from '@langchain/langgraph';
|
|
3
|
+
import { StateGraph, Annotation, START, END, interrupt, Command, GraphRecursionError, } from '@langchain/langgraph';
|
|
4
4
|
import { messagesStateReducer } from '@langchain/langgraph';
|
|
5
5
|
import { DB_PATH } from './db.js';
|
|
6
6
|
import { tools, maybePersistedOutput, initTools } from './tools.js';
|
|
@@ -113,6 +113,14 @@ function createAgentGraph(toolList) {
|
|
|
113
113
|
modelMessages = modelMessages.slice(0, 300);
|
|
114
114
|
// 修复因中断/截断导致的未完成 tool_calls
|
|
115
115
|
modelMessages = ensureCompleteToolCalls(modelMessages);
|
|
116
|
+
// 清理异常消息:某些 API(如 GLM)可能在流式响应中产生 role 为 undefined 的 ChatMessage
|
|
117
|
+
modelMessages = modelMessages.filter((msg) => {
|
|
118
|
+
const msgType = msg._getType?.();
|
|
119
|
+
if (msgType === 'generic' && msg.role === undefined) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
});
|
|
116
124
|
const messages = [new SystemMessage(systemPrompt), ...modelMessages];
|
|
117
125
|
const response = await modelWithTheseTools.invoke(messages, config);
|
|
118
126
|
return { messages: [response] };
|
|
@@ -287,32 +295,43 @@ async function _runAgent(compiledAgent, userMessage, onToken, onToolConfirmation
|
|
|
287
295
|
let fullResponse = '';
|
|
288
296
|
let usageMetadata;
|
|
289
297
|
let input = { messages: [new HumanMessage(userMessage)] };
|
|
298
|
+
const recursionLimit = 50;
|
|
290
299
|
while (true) {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
+
try {
|
|
301
|
+
const stream = await compiledAgent.stream(input, {
|
|
302
|
+
...config,
|
|
303
|
+
streamMode: 'messages',
|
|
304
|
+
signal,
|
|
305
|
+
recursionLimit,
|
|
306
|
+
});
|
|
307
|
+
for await (const chunk of stream) {
|
|
308
|
+
if (signal?.aborted) {
|
|
309
|
+
throw new Error('abort');
|
|
310
|
+
}
|
|
311
|
+
const message = chunk[0];
|
|
312
|
+
const metadata = chunk[1];
|
|
313
|
+
if (metadata?.langgraph_node !== 'model_request')
|
|
314
|
+
continue;
|
|
315
|
+
const msgUsage = message.usage_metadata;
|
|
316
|
+
if (msgUsage) {
|
|
317
|
+
usageMetadata = msgUsage;
|
|
318
|
+
}
|
|
319
|
+
// AIMessageChunk 的 content 在 message.content 属性上,不在 kwargs.content
|
|
320
|
+
const content = message.content ?? message.kwargs?.content ?? '';
|
|
321
|
+
const toolCallChunks = message.tool_call_chunks ?? [];
|
|
322
|
+
if (!content || toolCallChunks.length > 0)
|
|
323
|
+
continue;
|
|
324
|
+
onToken(content);
|
|
325
|
+
fullResponse += content;
|
|
300
326
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if (
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
usageMetadata = msgUsage;
|
|
327
|
+
}
|
|
328
|
+
catch (err) {
|
|
329
|
+
if (err instanceof GraphRecursionError) {
|
|
330
|
+
const hint = `我已经思考了 ${recursionLimit} 个步骤,仍未得到最终结果,我暂停歇会儿~ 你可以随时让我“继续”`;
|
|
331
|
+
onToken('\n\n' + hint);
|
|
332
|
+
return { response: fullResponse + '\n\n' + hint, usageMetadata };
|
|
308
333
|
}
|
|
309
|
-
|
|
310
|
-
const content = message.content ?? message.kwargs?.content ?? '';
|
|
311
|
-
const toolCallChunks = message.tool_call_chunks ?? [];
|
|
312
|
-
if (!content || toolCallChunks.length > 0)
|
|
313
|
-
continue;
|
|
314
|
-
onToken(content);
|
|
315
|
-
fullResponse += content;
|
|
334
|
+
throw err;
|
|
316
335
|
}
|
|
317
336
|
const state = await compiledAgent.getState(config);
|
|
318
337
|
const interruptedTask = state.tasks?.find((t) => t.interrupts?.length > 0);
|
package/dist/agent/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import { getModelContextLimit } from './context.js';
|
|
|
7
7
|
import { color, formatToolLog } from './colors.js';
|
|
8
8
|
import { threadId, commands } from './commands.js';
|
|
9
9
|
import { runSessionStartHooks } from './hooks.js';
|
|
10
|
+
import { getModelConfig } from './config.js';
|
|
10
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
12
|
const pkg = JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf-8'));
|
|
12
13
|
function createInterface() {
|
|
@@ -22,6 +23,7 @@ async function printBanner() {
|
|
|
22
23
|
const info = [
|
|
23
24
|
`${color.gray('Description')}: ${pkg.description}`,
|
|
24
25
|
`${color.gray('Version')}: ${pkg.version}`,
|
|
26
|
+
`${color.gray('Model')}: ${getModelConfig().model}`,
|
|
25
27
|
`${color.gray('Author')}: ${pkg.author}`,
|
|
26
28
|
`${color.gray('Docs')}: ${pkg.docs}`,
|
|
27
29
|
].join('\n');
|
|
@@ -47,9 +49,43 @@ function prompt(question) {
|
|
|
47
49
|
});
|
|
48
50
|
});
|
|
49
51
|
}
|
|
52
|
+
function createSpinner(text) {
|
|
53
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
54
|
+
let i = 0;
|
|
55
|
+
let timer = null;
|
|
56
|
+
let running = false;
|
|
57
|
+
const tick = () => {
|
|
58
|
+
i = (i + 1) % frames.length;
|
|
59
|
+
readline.cursorTo(process.stdout, 0);
|
|
60
|
+
process.stdout.write(color.gray(`${text} ${frames[i]}`));
|
|
61
|
+
};
|
|
62
|
+
return {
|
|
63
|
+
start() {
|
|
64
|
+
if (running)
|
|
65
|
+
return;
|
|
66
|
+
running = true;
|
|
67
|
+
i = 0;
|
|
68
|
+
readline.cursorTo(process.stdout, 0);
|
|
69
|
+
process.stdout.write(color.gray(`${text} ${frames[0]}`));
|
|
70
|
+
timer = setInterval(tick, 80);
|
|
71
|
+
},
|
|
72
|
+
stop() {
|
|
73
|
+
if (!running)
|
|
74
|
+
return;
|
|
75
|
+
running = false;
|
|
76
|
+
if (timer) {
|
|
77
|
+
clearInterval(timer);
|
|
78
|
+
timer = null;
|
|
79
|
+
}
|
|
80
|
+
readline.cursorTo(process.stdout, 0);
|
|
81
|
+
readline.clearLine(process.stdout, 0);
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
50
85
|
async function chat(userInput) {
|
|
51
86
|
const rl = createInterface();
|
|
52
|
-
|
|
87
|
+
// AI: 前缀延迟到第一个 token 输出,此前显示 spinner
|
|
88
|
+
// process.stdout.write('\n' + color.aiPrefix())
|
|
53
89
|
const controller = new AbortController();
|
|
54
90
|
// 监听 ESC 键,中断 AI 请求
|
|
55
91
|
const escListener = (_str, key) => {
|
|
@@ -62,18 +98,31 @@ async function chat(userInput) {
|
|
|
62
98
|
readline.emitKeypressEvents(process.stdin);
|
|
63
99
|
process.stdin.on('keypress', escListener);
|
|
64
100
|
let usageMetadata;
|
|
101
|
+
let isFirstToken = true;
|
|
102
|
+
const spinner = createSpinner('AI 思考中');
|
|
65
103
|
try {
|
|
104
|
+
spinner.start();
|
|
66
105
|
const result = await runAgentStream(userInput, (token) => {
|
|
106
|
+
if (isFirstToken) {
|
|
107
|
+
isFirstToken = false;
|
|
108
|
+
spinner.stop();
|
|
109
|
+
process.stdout.write('\n' + color.aiPrefix());
|
|
110
|
+
}
|
|
67
111
|
process.stdout.write(token);
|
|
68
112
|
}, async (toolCalls) => {
|
|
113
|
+
spinner.stop();
|
|
69
114
|
for (const call of toolCalls) {
|
|
70
115
|
console.log(formatToolLog(call.name, JSON.stringify(call.args)));
|
|
71
116
|
}
|
|
72
117
|
const answer = await new Promise((resolve) => {
|
|
73
118
|
rl.question('确认执行以上工具? (y/n): ', resolve);
|
|
74
119
|
});
|
|
75
|
-
|
|
76
|
-
answer.trim().toLowerCase() === 'yes'
|
|
120
|
+
const confirmed = answer.trim().toLowerCase() === 'y' ||
|
|
121
|
+
answer.trim().toLowerCase() === 'yes';
|
|
122
|
+
if (confirmed && isFirstToken) {
|
|
123
|
+
spinner.start();
|
|
124
|
+
}
|
|
125
|
+
return confirmed;
|
|
77
126
|
}, threadId, controller.signal);
|
|
78
127
|
usageMetadata = result.usageMetadata;
|
|
79
128
|
}
|
|
@@ -83,6 +132,9 @@ async function chat(userInput) {
|
|
|
83
132
|
}
|
|
84
133
|
}
|
|
85
134
|
finally {
|
|
135
|
+
if (isFirstToken) {
|
|
136
|
+
spinner.stop();
|
|
137
|
+
}
|
|
86
138
|
process.stdin.removeListener('keypress', escListener);
|
|
87
139
|
rl.close();
|
|
88
140
|
}
|
|
@@ -90,7 +142,7 @@ async function chat(userInput) {
|
|
|
90
142
|
const limit = getModelContextLimit();
|
|
91
143
|
const percentage = (usageMetadata.total_tokens / limit) * 100;
|
|
92
144
|
const percentageStr = percentage.toFixed(1);
|
|
93
|
-
const tokenText =
|
|
145
|
+
const tokenText = `${getModelConfig().model}, Tokens: ${usageMetadata.total_tokens.toLocaleString()} / ${limit.toLocaleString()} (${percentageStr}%)`;
|
|
94
146
|
if (percentage >= 80) {
|
|
95
147
|
process.stdout.write('\n' +
|
|
96
148
|
color.error(tokenText) +
|
package/dist/agent/config.js
CHANGED
|
@@ -36,7 +36,10 @@ export function getModelConfig() {
|
|
|
36
36
|
if (!config.model.baseURL) {
|
|
37
37
|
throw new Error(`配置文件 ${CONFIG_PATH} 中缺少 model.baseURL 字段,请参考 https://zhitalk.chat/#config 修改配置`);
|
|
38
38
|
}
|
|
39
|
-
return
|
|
39
|
+
return {
|
|
40
|
+
...config.model,
|
|
41
|
+
model: config.model.model.toLowerCase(),
|
|
42
|
+
};
|
|
40
43
|
}
|
|
41
44
|
export function getEnv(key) {
|
|
42
45
|
const config = loadConfig();
|
package/dist/agent/model.js
CHANGED
|
@@ -2,16 +2,21 @@ import { ChatOpenAI } from '@langchain/openai';
|
|
|
2
2
|
import { getModelConfig } from './config.js';
|
|
3
3
|
const modelConfig = getModelConfig();
|
|
4
4
|
export function createModel(options) {
|
|
5
|
+
const modelKwargs = {};
|
|
6
|
+
if (modelConfig.model.startsWith('kimi') ||
|
|
7
|
+
modelConfig.model.startsWith('minimax')) {
|
|
8
|
+
modelKwargs.thinking = { type: 'disabled' };
|
|
9
|
+
}
|
|
10
|
+
// GLM 模型的流式 function calling 存在兼容性问题,禁用流式输出
|
|
11
|
+
const isGlm = modelConfig.model.startsWith('glm');
|
|
5
12
|
return new ChatOpenAI({
|
|
6
13
|
model: modelConfig.model,
|
|
7
14
|
apiKey: modelConfig.apiKey,
|
|
8
15
|
configuration: {
|
|
9
16
|
baseURL: modelConfig.baseURL,
|
|
10
17
|
},
|
|
11
|
-
streaming: options?.streaming ?? true,
|
|
12
|
-
modelKwargs
|
|
13
|
-
thinking: { type: 'disabled' },
|
|
14
|
-
},
|
|
18
|
+
streaming: isGlm ? false : (options?.streaming ?? true),
|
|
19
|
+
modelKwargs,
|
|
15
20
|
});
|
|
16
21
|
}
|
|
17
22
|
export async function checkModel() {
|
package/package.json
CHANGED