flashclaw 1.7.0 → 1.8.0

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 (41) hide show
  1. package/README.md +31 -26
  2. package/dist/agent-runner.d.ts +10 -0
  3. package/dist/agent-runner.d.ts.map +1 -1
  4. package/dist/agent-runner.js +46 -55
  5. package/dist/agent-runner.js.map +1 -1
  6. package/dist/channel-manager.js +1 -1
  7. package/dist/channel-manager.js.map +1 -1
  8. package/dist/cli-ink.d.ts +11 -0
  9. package/dist/cli-ink.d.ts.map +1 -0
  10. package/dist/cli-ink.js +344 -0
  11. package/dist/cli-ink.js.map +1 -0
  12. package/dist/cli.js +9 -247
  13. package/dist/cli.js.map +1 -1
  14. package/dist/config.js +1 -1
  15. package/dist/config.js.map +1 -1
  16. package/dist/core/context-guard.d.ts +1 -1
  17. package/dist/core/context-guard.js +2 -2
  18. package/dist/core/memory.d.ts +16 -0
  19. package/dist/core/memory.d.ts.map +1 -1
  20. package/dist/core/memory.js +114 -0
  21. package/dist/core/memory.js.map +1 -1
  22. package/dist/core-api.d.ts +115 -0
  23. package/dist/core-api.d.ts.map +1 -0
  24. package/dist/core-api.js +210 -0
  25. package/dist/core-api.js.map +1 -0
  26. package/dist/health.js +1 -1
  27. package/dist/health.js.map +1 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +81 -50
  30. package/dist/index.js.map +1 -1
  31. package/dist/message-queue.js +1 -1
  32. package/dist/message-queue.js.map +1 -1
  33. package/dist/plugins/loader.js +4 -4
  34. package/dist/plugins/loader.js.map +1 -1
  35. package/dist/plugins/manager.js +3 -3
  36. package/dist/plugins/manager.js.map +1 -1
  37. package/dist/task-scheduler.js +1 -1
  38. package/dist/task-scheduler.js.map +1 -1
  39. package/package.json +4 -1
  40. package/plugins/cli-channel/index.ts +203 -19
  41. package/plugins/memory/index.ts +65 -15
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * CLI 渠道插件 - 终端交互渠道
3
3
  *
4
- * 作为 FlashClaw 的内置终端渠道,
5
- * 提供 CLI 命令客户端连接到服务
4
+ * 提供独立的 HTTP API 供 flashclaw cli 客户端连接。
5
+ * 通过核心 API 层(core-api)处理消息和命令,不依赖 web-ui。
6
6
  *
7
7
  * 使用方式:
8
8
  * 1. 启动服务: flashclaw start
@@ -11,39 +11,223 @@
11
11
 
12
12
  import type { ChannelPlugin, MessageHandler, PluginConfig, SendMessageResult } from '../../src/plugins/types.js';
13
13
  import { createLogger } from '../../src/logger.js';
14
+ import http from 'http';
14
15
 
15
16
  const logger = createLogger('CLI-Channel');
16
17
 
17
- const plugin: ChannelPlugin & {
18
- group: string;
19
- } = {
18
+ const DEFAULT_PORT = 3001;
19
+ let server: http.Server | null = null;
20
+ let cliPort = DEFAULT_PORT;
21
+
22
+ /**
23
+ * 获取核心 API(通过全局变量注入)
24
+ */
25
+ function getCoreApi() {
26
+ return (global as Record<string, unknown>).__flashclaw_core_api as typeof import('../../src/core-api.js') | undefined;
27
+ }
28
+
29
+ /**
30
+ * 解析 JSON 请求体
31
+ */
32
+ function parseBody(req: http.IncomingMessage): Promise<Record<string, unknown>> {
33
+ return new Promise((resolve, reject) => {
34
+ let body = '';
35
+ req.on('data', (chunk: Buffer) => { body += chunk.toString(); });
36
+ req.on('end', () => {
37
+ try {
38
+ resolve(body ? JSON.parse(body) : {});
39
+ } catch {
40
+ reject(new Error('Invalid JSON'));
41
+ }
42
+ });
43
+ req.on('error', reject);
44
+ });
45
+ }
46
+
47
+ /**
48
+ * 发送 JSON 响应
49
+ */
50
+ function sendJson(res: http.ServerResponse, status: number, data: unknown): void {
51
+ res.writeHead(status, {
52
+ 'Content-Type': 'application/json',
53
+ 'Access-Control-Allow-Origin': '*',
54
+ });
55
+ res.end(JSON.stringify(data));
56
+ }
57
+
58
+ /**
59
+ * 处理 HTTP 请求
60
+ */
61
+ async function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
62
+ const url = req.url || '/';
63
+ const method = req.method || 'GET';
64
+
65
+ // CORS preflight
66
+ if (method === 'OPTIONS') {
67
+ res.writeHead(204, {
68
+ 'Access-Control-Allow-Origin': '*',
69
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
70
+ 'Access-Control-Allow-Headers': 'Content-Type',
71
+ });
72
+ res.end();
73
+ return;
74
+ }
75
+
76
+ const api = getCoreApi();
77
+ if (!api) {
78
+ sendJson(res, 503, { error: 'Core API not ready' });
79
+ return;
80
+ }
81
+
82
+ try {
83
+ // GET /api/status
84
+ if (url === '/api/status' && method === 'GET') {
85
+ sendJson(res, 200, api.getStatus());
86
+ return;
87
+ }
88
+
89
+ // GET /api/chat/history?group=xxx
90
+ if (url.startsWith('/api/chat/history') && method === 'GET') {
91
+ const params = new URL(url, 'http://localhost').searchParams;
92
+ const group = params.get('group') || 'main';
93
+ const chatId = `${group}-chat`;
94
+ const messages = api.getHistory(chatId);
95
+ sendJson(res, 200, { success: true, messages });
96
+ return;
97
+ }
98
+
99
+ // POST /api/chat/clear
100
+ if (url === '/api/chat/clear' && method === 'POST') {
101
+ const body = await parseBody(req);
102
+ const group = (body.group as string) || 'main';
103
+ api.clearSession(group);
104
+ sendJson(res, 200, { success: true });
105
+ return;
106
+ }
107
+
108
+ // POST /api/chat/stream — 流式对话
109
+ if (url === '/api/chat/stream' && method === 'POST') {
110
+ const body = await parseBody(req);
111
+ const message = body.message as string;
112
+ const group = (body.group as string) || 'main';
113
+
114
+ if (!message) {
115
+ sendJson(res, 400, { error: 'message is required' });
116
+ return;
117
+ }
118
+
119
+ // 流式响应
120
+ res.writeHead(200, {
121
+ 'Content-Type': 'text/plain; charset=utf-8',
122
+ 'Transfer-Encoding': 'chunked',
123
+ 'Access-Control-Allow-Origin': '*',
124
+ 'Cache-Control': 'no-cache',
125
+ });
126
+
127
+ try {
128
+ const result = await api.chat({
129
+ message,
130
+ group,
131
+ userId: 'cli-user',
132
+ platform: 'cli-channel',
133
+ onToken: (chunk: string) => {
134
+ res.write(chunk);
135
+ },
136
+ onToolUse: (name: string, input: unknown) => {
137
+ res.write(`[TOOL:${JSON.stringify({ name, input })}]`);
138
+ },
139
+ });
140
+
141
+ // 发送 metrics
142
+ if (result.metrics) {
143
+ res.write(`[METRICS:${JSON.stringify({
144
+ durationMs: result.metrics.durationMs,
145
+ model: result.metrics.model,
146
+ inputTokens: result.metrics.usage?.inputTokens ?? null,
147
+ outputTokens: result.metrics.usage?.outputTokens ?? null,
148
+ })}]`);
149
+ }
150
+ } catch (err) {
151
+ res.write(`\n\n❌ 错误: ${err instanceof Error ? err.message : String(err)}`);
152
+ }
153
+
154
+ res.end();
155
+ return;
156
+ }
157
+
158
+ // POST /api/chat — 非流式对话
159
+ if (url === '/api/chat' && method === 'POST') {
160
+ const body = await parseBody(req);
161
+ const message = body.message as string;
162
+ const group = (body.group as string) || 'main';
163
+
164
+ if (!message) {
165
+ sendJson(res, 400, { error: 'message is required' });
166
+ return;
167
+ }
168
+
169
+ const result = await api.chat({ message, group, userId: 'cli-user', platform: 'cli-channel' });
170
+ sendJson(res, 200, { response: result.response, metrics: result.metrics });
171
+ return;
172
+ }
173
+
174
+ // POST /api/compact
175
+ if (url === '/api/compact' && method === 'POST') {
176
+ const body = await parseBody(req);
177
+ const group = (body.group as string) || 'main';
178
+ const chatId = `${group}-chat`;
179
+ const summary = await api.compactSession(chatId, group, 'cli-user', 'cli-channel');
180
+ sendJson(res, 200, { success: true, summary });
181
+ return;
182
+ }
183
+
184
+ // 404
185
+ sendJson(res, 404, { error: 'Not Found' });
186
+ } catch (err) {
187
+ logger.error({ err, url }, 'CLI-Channel API 错误');
188
+ sendJson(res, 500, { error: err instanceof Error ? err.message : 'Internal Server Error' });
189
+ }
190
+ }
191
+
192
+ const plugin: ChannelPlugin = {
20
193
  name: 'cli-channel',
21
- version: '1.0.0',
22
- group: 'cli-default',
194
+ version: '2.0.0',
23
195
 
24
- async init(_config: PluginConfig): Promise<void> {
25
- // CLI 渠道不需要特殊配置
196
+ async init(config: PluginConfig): Promise<void> {
197
+ cliPort = Number(config.port || process.env.CLI_PORT || DEFAULT_PORT);
26
198
  },
27
199
 
28
200
  onMessage(_handler: MessageHandler): void {
29
- // CLI 是客户端模式,由 flashclaw cli 命令连接
30
- // 消息通过 HTTP API 传输
201
+ // CLI 渠道通过 HTTP API 接收消息,不需要 onMessage handler
31
202
  },
32
203
 
33
204
  async start(): Promise<void> {
34
- // CLI 渠道是客户端模式,不绑定终端
35
- // 用户通过 flashclaw cli 命令连接
36
- // 消息通过 web-ui 的 /api/chat 接口传输
37
- logger.info('CLI 渠道已就绪,使用 flashclaw cli 连接服务');
205
+ server = http.createServer((req, res) => {
206
+ handleRequest(req, res).catch(err => {
207
+ logger.error({ err }, 'CLI-Channel 请求处理失败');
208
+ if (!res.headersSent) {
209
+ sendJson(res, 500, { error: 'Internal Server Error' });
210
+ }
211
+ });
212
+ });
213
+
214
+ server.listen(cliPort, () => {
215
+ logger.debug({ port: cliPort }, '⚡ CLI 渠道 API 已启动');
216
+ });
38
217
  },
39
218
 
40
219
  async stop(): Promise<void> {
41
- // 清理资源
220
+ if (server) {
221
+ await new Promise<void>((resolve) => {
222
+ server!.close(() => resolve());
223
+ });
224
+ server = null;
225
+ logger.info('⚡ CLI 渠道 API 已停止');
226
+ }
42
227
  },
43
228
 
44
- async sendMessage(_chatId: string, content: string): Promise<SendMessageResult> {
45
- // CLI 渠道的消息由 flashclaw cli 命令处理
46
- // 这里不需要实现(消息通过 API 传输)
229
+ async sendMessage(_chatId: string, _content: string): Promise<SendMessageResult> {
230
+ // CLI 渠道的消息通过 HTTP 流式响应直接返回,不需要主动推送
47
231
  return { success: true };
48
232
  }
49
233
  };
@@ -5,16 +5,18 @@
5
5
 
6
6
  import { ToolPlugin, ToolContext, ToolResult } from '../../src/plugins/types.js';
7
7
  import { getMemoryManager } from '../../src/core/memory.js';
8
+ import * as fs from 'node:fs';
9
+ import * as path from 'node:path';
8
10
 
9
11
  /**
10
12
  * 记忆操作参数
11
13
  */
12
14
  interface MemoryParams {
13
- /** 操作类型:remember(记住)或 recall(回忆) */
14
- action: 'remember' | 'recall';
15
+ /** 操作类型:remember(记住)、recall(回忆)或 log(追加每日日志) */
16
+ action: 'remember' | 'recall' | 'log';
15
17
  /** 记忆键(remember 必需,recall 可选) */
16
18
  key?: string;
17
- /** 记忆值(remember 必需) */
19
+ /** 记忆值(remember 必需)/ 日志内容(log 必需) */
18
20
  value?: string;
19
21
  /** 作用域:user(用户级别,跨会话共享)或 group(会话级别,默认) */
20
22
  scope?: 'user' | 'group';
@@ -22,27 +24,28 @@ interface MemoryParams {
22
24
 
23
25
  const plugin: ToolPlugin = {
24
26
  name: 'memory',
25
- version: '1.0.0',
26
- description: '长期记忆管理,可以记住和回忆重要信息',
27
+ version: '1.1.0',
28
+ description: '长期记忆管理,可以记住、回忆重要信息,以及写入每日日志',
27
29
 
28
30
  schema: {
29
31
  name: 'memory',
30
- description: `管理长期记忆。支持两种操作:
31
- - remember: 保存重要信息到长期记忆(用户偏好、重要事实等)
32
- - recall: 回忆之前保存的信息
32
+ description: `管理长期记忆和每日日志。
33
33
 
34
- 支持两种作用域:
35
- - user: 用户级别记忆,跨所有会话共享(推荐用于个人偏好)
36
- - group: 会话级别记忆,仅在当前会话有效(默认)
34
+ **何时用 remember**: 保存持久事实(姓名、偏好、配置等),需要 key 和 value
35
+ **何时用 recall**: 查询之前保存的事实
36
+ **何时用 log**: 记录事件、笔记、动态("今天做了XX"、"开了会"、"学了XX"),自动按日期归档,无需 key
37
37
 
38
- 记忆会持久化到文件,跨会话保持。`,
38
+ 示例:
39
+ - "记住我叫张三" → remember(key="name", value="张三")
40
+ - "帮我记录今天开了会" → log(value="今天开了会")
41
+ - "我叫什么" → recall(key="name")`,
39
42
  input_schema: {
40
43
  type: 'object',
41
44
  properties: {
42
45
  action: {
43
46
  type: 'string',
44
- enum: ['remember', 'recall'],
45
- description: 'remember 保存信息,recall 回忆信息'
47
+ enum: ['remember', 'recall', 'log'],
48
+ description: 'remember 保存信息,recall 回忆信息,log 追加每日日志'
46
49
  },
47
50
  key: {
48
51
  type: 'string',
@@ -180,9 +183,56 @@ const plugin: ToolPlugin = {
180
183
  }
181
184
  }
182
185
 
186
+ if (action === 'log') {
187
+ // 追加每日日志
188
+ if (!value || typeof value !== 'string') {
189
+ return {
190
+ success: false,
191
+ error: 'log 操作需要提供 value(日志内容)'
192
+ };
193
+ }
194
+
195
+ try {
196
+ const mm = getMemoryManager();
197
+ const memoryDir = (mm as unknown as { config: { memoryDir: string } }).config.memoryDir;
198
+ const logsDir = path.join(memoryDir, 'daily');
199
+ if (!fs.existsSync(logsDir)) {
200
+ fs.mkdirSync(logsDir, { recursive: true });
201
+ }
202
+
203
+ const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
204
+ const logFile = path.join(logsDir, `${today}.md`);
205
+ const time = new Date().toLocaleTimeString('zh-CN', { hour12: false });
206
+ const entry = `- [${time}] ${value}\n`;
207
+
208
+ // 如果文件不存在,添加标题
209
+ if (!fs.existsSync(logFile)) {
210
+ fs.writeFileSync(logFile, `# ${today} 日志\n\n${entry}`, 'utf-8');
211
+ } else {
212
+ fs.appendFileSync(logFile, entry, 'utf-8');
213
+ }
214
+
215
+ return {
216
+ success: true,
217
+ data: {
218
+ action: 'logged',
219
+ date: today,
220
+ time,
221
+ content: value,
222
+ message: `已记录到 ${today} 日志`
223
+ }
224
+ };
225
+ } catch (error) {
226
+ return {
227
+ success: false,
228
+ error: `写入日志失败: ${error instanceof Error ? error.message : String(error)}`
229
+ };
230
+ }
231
+ }
232
+
183
233
  return {
184
234
  success: false,
185
- error: 'action 必须是 remember 或 recall'
235
+ error: 'action 必须是 remember、recalllog'
186
236
  };
187
237
  }
188
238
  };