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.
- package/README.md +31 -26
- package/dist/agent-runner.d.ts +10 -0
- package/dist/agent-runner.d.ts.map +1 -1
- package/dist/agent-runner.js +46 -55
- package/dist/agent-runner.js.map +1 -1
- package/dist/channel-manager.js +1 -1
- package/dist/channel-manager.js.map +1 -1
- package/dist/cli-ink.d.ts +11 -0
- package/dist/cli-ink.d.ts.map +1 -0
- package/dist/cli-ink.js +344 -0
- package/dist/cli-ink.js.map +1 -0
- package/dist/cli.js +9 -247
- package/dist/cli.js.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/core/context-guard.d.ts +1 -1
- package/dist/core/context-guard.js +2 -2
- package/dist/core/memory.d.ts +16 -0
- package/dist/core/memory.d.ts.map +1 -1
- package/dist/core/memory.js +114 -0
- package/dist/core/memory.js.map +1 -1
- package/dist/core-api.d.ts +115 -0
- package/dist/core-api.d.ts.map +1 -0
- package/dist/core-api.js +210 -0
- package/dist/core-api.js.map +1 -0
- package/dist/health.js +1 -1
- package/dist/health.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +81 -50
- package/dist/index.js.map +1 -1
- package/dist/message-queue.js +1 -1
- package/dist/message-queue.js.map +1 -1
- package/dist/plugins/loader.js +4 -4
- package/dist/plugins/loader.js.map +1 -1
- package/dist/plugins/manager.js +3 -3
- package/dist/plugins/manager.js.map +1 -1
- package/dist/task-scheduler.js +1 -1
- package/dist/task-scheduler.js.map +1 -1
- package/package.json +4 -1
- package/plugins/cli-channel/index.ts +203 -19
- package/plugins/memory/index.ts +65 -15
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLI 渠道插件 - 终端交互渠道
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
18
|
-
|
|
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: '
|
|
22
|
-
group: 'cli-default',
|
|
194
|
+
version: '2.0.0',
|
|
23
195
|
|
|
24
|
-
async init(
|
|
25
|
-
|
|
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
|
|
30
|
-
// 消息通过 HTTP API 传输
|
|
201
|
+
// CLI 渠道通过 HTTP API 接收消息,不需要 onMessage handler
|
|
31
202
|
},
|
|
32
203
|
|
|
33
204
|
async start(): Promise<void> {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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,
|
|
45
|
-
// CLI
|
|
46
|
-
// 这里不需要实现(消息通过 API 传输)
|
|
229
|
+
async sendMessage(_chatId: string, _content: string): Promise<SendMessageResult> {
|
|
230
|
+
// CLI 渠道的消息通过 HTTP 流式响应直接返回,不需要主动推送
|
|
47
231
|
return { success: true };
|
|
48
232
|
}
|
|
49
233
|
};
|
package/plugins/memory/index.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
36
|
-
|
|
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 或
|
|
235
|
+
error: 'action 必须是 remember、recall 或 log'
|
|
186
236
|
};
|
|
187
237
|
}
|
|
188
238
|
};
|