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 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
- ## Usage
5
+ 📖 你可以免费围观项目进展,也可以加入学习 [跟双越老师从 0 开发一个 AI Agent 智能体](https://www.huashuiai.com/pub/ai-agent-camp-new)
6
6
 
7
- Docs https://zhitalk.chat/
7
+ ## Usage
8
8
 
9
- Install
9
+ 使用 npm 全局安装。注意,Windows 用户使用管理员打开 cmd 再执行。
10
10
 
11
11
  ```
12
12
  npm install zhitalk -g
13
13
  ```
14
14
 
15
- Run in terminal
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
@@ -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
- const stream = await compiledAgent.stream(input, {
292
- ...config,
293
- streamMode: 'messages',
294
- signal,
295
- recursionLimit: 50,
296
- });
297
- for await (const chunk of stream) {
298
- if (signal?.aborted) {
299
- throw new Error('abort');
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
- const message = chunk[0];
302
- const metadata = chunk[1];
303
- if (metadata?.langgraph_node !== 'model_request')
304
- continue;
305
- const msgUsage = message.usage_metadata;
306
- if (msgUsage) {
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
- // AIMessageChunk 的 content 在 message.content 属性上,不在 kwargs.content
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
- process.stdout.write('\n' + color.aiPrefix());
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
- return (answer.trim().toLowerCase() === 'y' ||
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 = `Tokens: ${usageMetadata.total_tokens.toLocaleString()} / ${limit.toLocaleString()} (${percentageStr}%)`;
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) +
@@ -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 config.model;
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();
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zhitalk",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "description": "A personal AI Agent like OpenClaw, including tools, skills, memory, hook, sub-agent, MCP server, etc. Run in the terminal.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",