flashclaw 1.5.0 → 1.6.1
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 +11 -7
- package/dist/agent-runner.d.ts +2 -0
- package/dist/agent-runner.d.ts.map +1 -1
- package/dist/agent-runner.js +55 -30
- package/dist/agent-runner.js.map +1 -1
- package/dist/channel-manager.d.ts +19 -0
- package/dist/channel-manager.d.ts.map +1 -0
- package/dist/channel-manager.js +132 -0
- package/dist/channel-manager.js.map +1 -0
- package/dist/cli.js +252 -0
- package/dist/cli.js.map +1 -1
- package/dist/config-schema.d.ts +4 -1
- package/dist/config-schema.d.ts.map +1 -1
- package/dist/config-schema.js +7 -2
- package/dist/config-schema.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -1
- package/dist/config.js.map +1 -1
- package/dist/core/api-client.d.ts +12 -14
- package/dist/core/api-client.d.ts.map +1 -1
- package/dist/core/api-client.js +100 -58
- package/dist/core/api-client.js.map +1 -1
- package/dist/core/memory.d.ts +9 -2
- package/dist/core/memory.d.ts.map +1 -1
- package/dist/core/memory.js +26 -8
- package/dist/core/memory.js.map +1 -1
- package/dist/index.d.ts +4 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +77 -231
- package/dist/index.js.map +1 -1
- package/dist/message-queue.d.ts.map +1 -1
- package/dist/message-queue.js +1 -0
- package/dist/message-queue.js.map +1 -1
- package/dist/paths.d.ts +7 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +11 -0
- package/dist/paths.js.map +1 -1
- package/dist/plugins/loader.d.ts.map +1 -1
- package/dist/plugins/loader.js +8 -2
- package/dist/plugins/loader.js.map +1 -1
- package/dist/plugins/manager.d.ts +24 -1
- package/dist/plugins/manager.d.ts.map +1 -1
- package/dist/plugins/manager.js +70 -5
- package/dist/plugins/manager.js.map +1 -1
- package/dist/plugins/types.d.ts +76 -2
- package/dist/plugins/types.d.ts.map +1 -1
- package/dist/plugins/types.js +11 -0
- package/dist/plugins/types.js.map +1 -1
- package/dist/utils/network.d.ts +41 -0
- package/dist/utils/network.d.ts.map +1 -0
- package/dist/utils/network.js +129 -0
- package/dist/utils/network.js.map +1 -0
- package/package.json +7 -2
- package/plugins/anthropic-provider/index.ts +461 -0
- package/plugins/anthropic-provider/plugin.json +7 -0
- package/plugins/cli-channel/index.ts +51 -0
- package/plugins/cli-channel/plugin.json +8 -0
- package/plugins/feishu/index.ts +0 -1001
- package/plugins/feishu/plugin.json +0 -29
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlashClaw 网络工具函数
|
|
3
|
+
* IP 检测、URL 提取、文本截断等
|
|
4
|
+
*/
|
|
5
|
+
import { isIP } from 'net';
|
|
6
|
+
// ==================== URL 提取 ====================
|
|
7
|
+
const WEB_FETCH_URL_RE = /https?:\/\/[^\s<>()]+/i;
|
|
8
|
+
const WEB_FETCH_DOMAIN_RE = /(?:^|[^A-Za-z0-9.-])((?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,})(:\d{2,5})?(\/[^\s<>()]*)?/i;
|
|
9
|
+
const TRAILING_PUNCT_RE = /[)\],.。,;;!!??]+$/;
|
|
10
|
+
/**
|
|
11
|
+
* 从文本中提取第一个 URL
|
|
12
|
+
* 支持完整 URL 和裸域名(自动添加 https://)
|
|
13
|
+
*/
|
|
14
|
+
export function extractFirstUrl(text) {
|
|
15
|
+
const match = text.match(WEB_FETCH_URL_RE);
|
|
16
|
+
if (match) {
|
|
17
|
+
return match[0].replace(TRAILING_PUNCT_RE, '');
|
|
18
|
+
}
|
|
19
|
+
const domainMatch = text.match(WEB_FETCH_DOMAIN_RE);
|
|
20
|
+
if (!domainMatch)
|
|
21
|
+
return null;
|
|
22
|
+
const host = domainMatch[1];
|
|
23
|
+
const port = domainMatch[2] ?? '';
|
|
24
|
+
const urlPath = domainMatch[3] ?? '';
|
|
25
|
+
const candidate = `https://${host}${port}${urlPath}`;
|
|
26
|
+
return candidate.replace(TRAILING_PUNCT_RE, '');
|
|
27
|
+
}
|
|
28
|
+
// ==================== IP 安全检测 ====================
|
|
29
|
+
/**
|
|
30
|
+
* 检测 IPv4 是否为私有地址
|
|
31
|
+
*/
|
|
32
|
+
export function isPrivateIpv4(ip) {
|
|
33
|
+
const parts = ip.split('.').map((part) => Number(part));
|
|
34
|
+
if (parts.length !== 4 || parts.some((part) => Number.isNaN(part)))
|
|
35
|
+
return false;
|
|
36
|
+
const [a, b] = parts;
|
|
37
|
+
if (a === 0)
|
|
38
|
+
return true;
|
|
39
|
+
if (a === 10)
|
|
40
|
+
return true;
|
|
41
|
+
if (a === 127)
|
|
42
|
+
return true;
|
|
43
|
+
if (a === 169 && b === 254)
|
|
44
|
+
return true;
|
|
45
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
46
|
+
return true;
|
|
47
|
+
if (a === 192 && b === 168)
|
|
48
|
+
return true;
|
|
49
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
50
|
+
return true;
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 检测 IPv6 是否为私有地址
|
|
55
|
+
*/
|
|
56
|
+
export function isPrivateIpv6(ip) {
|
|
57
|
+
const normalized = ip.toLowerCase();
|
|
58
|
+
if (normalized === '::' || normalized === '::1')
|
|
59
|
+
return true;
|
|
60
|
+
if (normalized.startsWith('fe80:'))
|
|
61
|
+
return true;
|
|
62
|
+
if (normalized.startsWith('fec0:'))
|
|
63
|
+
return true;
|
|
64
|
+
if (normalized.startsWith('fc') || normalized.startsWith('fd'))
|
|
65
|
+
return true;
|
|
66
|
+
if (normalized.includes('::ffff:')) {
|
|
67
|
+
const ipv4Part = normalized.split('::ffff:')[1];
|
|
68
|
+
if (ipv4Part && isPrivateIpv4(ipv4Part))
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 检测 IP 是否为私有地址(自动判断 IPv4/IPv6)
|
|
75
|
+
*/
|
|
76
|
+
export function isPrivateIp(ip) {
|
|
77
|
+
const family = isIP(ip);
|
|
78
|
+
if (family === 4)
|
|
79
|
+
return isPrivateIpv4(ip);
|
|
80
|
+
if (family === 6)
|
|
81
|
+
return isPrivateIpv6(ip);
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 检测主机名是否为被阻止的内部地址
|
|
86
|
+
*/
|
|
87
|
+
export function isBlockedHostname(hostname) {
|
|
88
|
+
const normalized = hostname.trim().toLowerCase();
|
|
89
|
+
if (normalized === 'localhost')
|
|
90
|
+
return true;
|
|
91
|
+
return (normalized.endsWith('.localhost') ||
|
|
92
|
+
normalized.endsWith('.local') ||
|
|
93
|
+
normalized.endsWith('.internal'));
|
|
94
|
+
}
|
|
95
|
+
// ==================== 文本工具 ====================
|
|
96
|
+
/**
|
|
97
|
+
* 估算 base64 编码内容的原始字节数
|
|
98
|
+
*/
|
|
99
|
+
export function estimateBase64Bytes(content) {
|
|
100
|
+
if (!content)
|
|
101
|
+
return null;
|
|
102
|
+
const raw = content.startsWith('data:') ? content.split(',')[1] ?? '' : content;
|
|
103
|
+
const normalized = raw.replace(/\s+/g, '');
|
|
104
|
+
if (!normalized)
|
|
105
|
+
return 0;
|
|
106
|
+
const padding = normalized.endsWith('==') ? 2 : normalized.endsWith('=') ? 1 : 0;
|
|
107
|
+
return Math.max(0, Math.floor((normalized.length * 3) / 4) - padding);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* 截断文本到指定长度
|
|
111
|
+
*/
|
|
112
|
+
export function truncateText(text, maxLength) {
|
|
113
|
+
if (text.length <= maxLength) {
|
|
114
|
+
return { text, truncated: false };
|
|
115
|
+
}
|
|
116
|
+
return { text: `${text.slice(0, maxLength)}\n\n...(内容已截断)`, truncated: true };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* XML 转义(用于消息构建)
|
|
120
|
+
*/
|
|
121
|
+
export function escapeXml(s) {
|
|
122
|
+
return s
|
|
123
|
+
.replace(/&/g, '&')
|
|
124
|
+
.replace(/</g, '<')
|
|
125
|
+
.replace(/>/g, '>')
|
|
126
|
+
.replace(/"/g, '"')
|
|
127
|
+
.replace(/'/g, ''');
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=network.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"network.js","sourceRoot":"","sources":["../../src/utils/network.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAE3B,mDAAmD;AAEnD,MAAM,gBAAgB,GAAG,wBAAwB,CAAC;AAClD,MAAM,mBAAmB,GAAG,mFAAmF,CAAC;AAChH,MAAM,iBAAiB,GAAG,mBAAmB,CAAC;AAE9C;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC;IAC3C,IAAI,KAAK,EAAE,CAAC;QACV,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,iBAAiB,EAAE,EAAE,CAAC,CAAC;IACjD,CAAC;IAED,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAC;IACpD,IAAI,CAAC,WAAW;QAAE,OAAO,IAAI,CAAC;IAC9B,MAAM,IAAI,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;IAC5B,MAAM,IAAI,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAClC,MAAM,OAAO,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACrC,MAAM,SAAS,GAAG,WAAW,IAAI,GAAG,IAAI,GAAG,OAAO,EAAE,CAAC;IACrD,OAAO,SAAS,CAAC,OAAO,CAAC,iBAAiB,EAAE,EAAE,CAAC,CAAC;AAClD,CAAC;AAED,oDAAoD;AAEpD;;GAEG;AACH,MAAM,UAAU,aAAa,CAAC,EAAU;IACtC,MAAM,KAAK,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;IACxD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IAEjF,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,KAAK,CAAC;IACrB,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACzB,IAAI,CAAC,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAC1B,IAAI,CAAC,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IAC3B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IACxC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE;QAAE,OAAO,IAAI,CAAC;IACjD,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IACxC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,GAAG;QAAE,OAAO,IAAI,CAAC;IAClD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,aAAa,CAAC,EAAU;IACtC,MAAM,UAAU,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC;IACpC,IAAI,UAAU,KAAK,IAAI,IAAI,UAAU,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IAC7D,IAAI,UAAU,CAAC,UAAU,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC;IAChD,IAAI,UAAU,CAAC,UAAU,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC;IAChD,IAAI,UAAU,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAE5E,IAAI,UAAU,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;QACnC,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;QAChD,IAAI,QAAQ,IAAI,aAAa,CAAC,QAAQ,CAAC;YAAE,OAAO,IAAI,CAAC;IACvD,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,WAAW,CAAC,EAAU;IACpC,MAAM,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC;IACxB,IAAI,MAAM,KAAK,CAAC;QAAE,OAAO,aAAa,CAAC,EAAE,CAAC,CAAC;IAC3C,IAAI,MAAM,KAAK,CAAC;QAAE,OAAO,aAAa,CAAC,EAAE,CAAC,CAAC;IAC3C,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAAgB;IAChD,MAAM,UAAU,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACjD,IAAI,UAAU,KAAK,WAAW;QAAE,OAAO,IAAI,CAAC;IAC5C,OAAO,CACL,UAAU,CAAC,QAAQ,CAAC,YAAY,CAAC;QACjC,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAC7B,UAAU,CAAC,QAAQ,CAAC,WAAW,CAAC,CACjC,CAAC;AACJ,CAAC;AAED,iDAAiD;AAEjD;;GAEG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAAe;IACjD,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAC1B,MAAM,GAAG,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;IAChF,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC3C,IAAI,CAAC,UAAU;QAAE,OAAO,CAAC,CAAC;IAC1B,MAAM,OAAO,GAAG,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACjF,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC;AACxE,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,SAAiB;IAC1D,IAAI,IAAI,CAAC,MAAM,IAAI,SAAS,EAAE,CAAC;QAC7B,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;IACpC,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,gBAAgB,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;AAChF,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,SAAS,CAAC,CAAS;IACjC,OAAO,CAAC;SACL,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAC7B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flashclaw",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"description": "⚡ 闪电龙虾 - 快如闪电的 AI 助手,乐高式插件架构",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -62,22 +62,27 @@
|
|
|
62
62
|
"dotenv": "^17.2.3",
|
|
63
63
|
"hono": "^4.6.14",
|
|
64
64
|
"jiti": "^2.6.1",
|
|
65
|
+
"openai": "^6.25.0",
|
|
65
66
|
"p-limit": "^7.2.0",
|
|
66
67
|
"pino": "^9.6.0",
|
|
67
68
|
"pino-pretty": "^13.0.0",
|
|
68
69
|
"undici": "^7.20.0",
|
|
70
|
+
"ws": "^8.18.0",
|
|
69
71
|
"zod": "^4.3.6"
|
|
70
72
|
},
|
|
71
73
|
"optionalDependencies": {
|
|
72
|
-
"playwright-core": "^1.58.1",
|
|
73
74
|
"cheerio": "^1.2.0",
|
|
74
75
|
"html-to-text": "^9.0.5",
|
|
76
|
+
"playwright-core": "^1.58.1",
|
|
75
77
|
"turndown": "^7.2.2"
|
|
76
78
|
},
|
|
77
79
|
"devDependencies": {
|
|
78
80
|
"@types/better-sqlite3": "^7.6.12",
|
|
79
81
|
"@types/node": "^22.10.0",
|
|
80
82
|
"@types/ws": "^8.18.1",
|
|
83
|
+
"alpinejs": "^3.13.3",
|
|
84
|
+
"htmx.org": "^1.9.10",
|
|
85
|
+
"marked": "^15.0.4",
|
|
81
86
|
"tsx": "^4.19.0",
|
|
82
87
|
"typescript": "^5.7.0",
|
|
83
88
|
"vitest": "^4.0.18"
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic AI Provider 插件
|
|
3
|
+
* 实现 AIProviderPlugin 接口,支持 Claude 等模型
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
7
|
+
import type {
|
|
8
|
+
AIProviderPlugin,
|
|
9
|
+
ChatMessage,
|
|
10
|
+
ChatOptions,
|
|
11
|
+
StreamEvent,
|
|
12
|
+
ToolExecutor,
|
|
13
|
+
HeartbeatCallback,
|
|
14
|
+
PluginConfig,
|
|
15
|
+
ImageBlock,
|
|
16
|
+
TextBlock,
|
|
17
|
+
} from '../../src/plugins/types';
|
|
18
|
+
|
|
19
|
+
// ==================== 内部状态 ====================
|
|
20
|
+
|
|
21
|
+
let client: Anthropic | null = null;
|
|
22
|
+
let model: string = 'claude-sonnet-4-20250514';
|
|
23
|
+
|
|
24
|
+
// ==================== 内部常量和工具函数 ====================
|
|
25
|
+
|
|
26
|
+
const MAX_TOOL_CALL_DEPTH = 20;
|
|
27
|
+
const MAX_TOOL_RESULT_CHARS = 4000;
|
|
28
|
+
const KEEP_RECENT_TOOL_ROUNDS = 2;
|
|
29
|
+
|
|
30
|
+
function truncateToolResult(content: string, maxChars: number): string {
|
|
31
|
+
if (content.length <= maxChars) return content;
|
|
32
|
+
return content.slice(0, maxChars) + `\n...(内容已截断,原始 ${content.length} 字符)`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function compressToolHistory(
|
|
36
|
+
messages: Anthropic.MessageParam[],
|
|
37
|
+
keepRecentRounds: number,
|
|
38
|
+
): Anthropic.MessageParam[] {
|
|
39
|
+
const toolRoundIndices: number[] = [];
|
|
40
|
+
for (let i = 0; i < messages.length; i++) {
|
|
41
|
+
const msg = messages[i];
|
|
42
|
+
if (msg.role === 'assistant' && Array.isArray(msg.content)) {
|
|
43
|
+
const hasToolUse = (msg.content as Anthropic.ContentBlock[]).some(
|
|
44
|
+
(b) => b.type === 'tool_use'
|
|
45
|
+
);
|
|
46
|
+
if (hasToolUse) toolRoundIndices.push(i);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (toolRoundIndices.length <= keepRecentRounds) return messages;
|
|
51
|
+
|
|
52
|
+
const compressCount = toolRoundIndices.length - keepRecentRounds;
|
|
53
|
+
const toCompress = new Set(toolRoundIndices.slice(0, compressCount));
|
|
54
|
+
|
|
55
|
+
const result: Anthropic.MessageParam[] = [];
|
|
56
|
+
for (let i = 0; i < messages.length; i++) {
|
|
57
|
+
const msg = messages[i];
|
|
58
|
+
|
|
59
|
+
if (toCompress.has(i) && msg.role === 'assistant' && Array.isArray(msg.content)) {
|
|
60
|
+
const summaryParts: string[] = [];
|
|
61
|
+
for (const block of msg.content as Anthropic.ContentBlock[]) {
|
|
62
|
+
if (block.type === 'tool_use') {
|
|
63
|
+
const inputStr = JSON.stringify(block.input);
|
|
64
|
+
const inputPreview = inputStr.length > 80 ? inputStr.slice(0, 80) + '...' : inputStr;
|
|
65
|
+
summaryParts.push(`[已执行工具 ${block.name}(${inputPreview})]`);
|
|
66
|
+
} else if (block.type === 'text' && (block as Anthropic.TextBlock).text) {
|
|
67
|
+
summaryParts.push((block as Anthropic.TextBlock).text);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
result.push({ role: 'assistant', content: summaryParts.join('\n') || '[工具调用]' });
|
|
71
|
+
|
|
72
|
+
if (i + 1 < messages.length && messages[i + 1].role === 'user') {
|
|
73
|
+
const nextMsg = messages[i + 1];
|
|
74
|
+
if (Array.isArray(nextMsg.content)) {
|
|
75
|
+
const resultParts: string[] = [];
|
|
76
|
+
for (const block of nextMsg.content as Anthropic.ToolResultBlockParam[]) {
|
|
77
|
+
if (block.type === 'tool_result') {
|
|
78
|
+
const contentStr = typeof block.content === 'string' ? block.content : '';
|
|
79
|
+
const preview = contentStr.length > 100 ? contentStr.slice(0, 100) + '...' : contentStr;
|
|
80
|
+
resultParts.push(block.is_error ? `[失败: ${preview}]` : `[成功: ${preview}]`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
result.push({ role: 'user', content: resultParts.join('\n') || '[工具结果]' });
|
|
84
|
+
i++;
|
|
85
|
+
} else {
|
|
86
|
+
result.push(nextMsg);
|
|
87
|
+
i++;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
result.push(msg);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function extractText(response: Anthropic.Message): string {
|
|
99
|
+
const textBlocks = response.content.filter(
|
|
100
|
+
(block): block is Anthropic.TextBlock => block.type === 'text'
|
|
101
|
+
);
|
|
102
|
+
return textBlocks.map(block => block.text).join('');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function streamFollowUp(
|
|
106
|
+
messages: Anthropic.MessageParam[],
|
|
107
|
+
options?: ChatOptions,
|
|
108
|
+
heartbeat?: HeartbeatCallback
|
|
109
|
+
): Promise<Anthropic.Message> {
|
|
110
|
+
if (!client) {
|
|
111
|
+
throw new Error('Provider not initialized. Call init() first.');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const params: Anthropic.MessageCreateParams = {
|
|
115
|
+
model: model,
|
|
116
|
+
max_tokens: options?.maxTokens ?? 4096,
|
|
117
|
+
messages,
|
|
118
|
+
stream: true,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
if (options?.system) {
|
|
122
|
+
params.system = options.system;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (options?.tools && options.tools.length > 0) {
|
|
126
|
+
params.tools = options.tools as unknown as Anthropic.Tool[];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const stream = await client.messages.create(params);
|
|
130
|
+
|
|
131
|
+
let finalMessage: Anthropic.Message | null = null;
|
|
132
|
+
const contentBlocks: Array<Anthropic.TextBlock | Anthropic.ToolUseBlock> = [];
|
|
133
|
+
const partialJsonParts = new Map<number, string[]>();
|
|
134
|
+
|
|
135
|
+
for await (const event of stream as AsyncIterable<Anthropic.MessageStreamEvent>) {
|
|
136
|
+
heartbeat?.();
|
|
137
|
+
|
|
138
|
+
if (event.type === 'message_start') {
|
|
139
|
+
finalMessage = event.message as Anthropic.Message;
|
|
140
|
+
} else if (event.type === 'content_block_start') {
|
|
141
|
+
const block = event.content_block;
|
|
142
|
+
if (block.type === 'text') {
|
|
143
|
+
contentBlocks[event.index] = { type: 'text' as const, text: '', citations: null };
|
|
144
|
+
} else if (block.type === 'tool_use') {
|
|
145
|
+
contentBlocks[event.index] = {
|
|
146
|
+
type: 'tool_use' as const,
|
|
147
|
+
id: block.id,
|
|
148
|
+
name: block.name,
|
|
149
|
+
input: {},
|
|
150
|
+
};
|
|
151
|
+
partialJsonParts.set(event.index, []);
|
|
152
|
+
}
|
|
153
|
+
} else if (event.type === 'content_block_delta') {
|
|
154
|
+
const delta = event.delta;
|
|
155
|
+
if ('text' in delta) {
|
|
156
|
+
const block = contentBlocks[event.index];
|
|
157
|
+
if (block?.type === 'text') {
|
|
158
|
+
block.text += delta.text;
|
|
159
|
+
}
|
|
160
|
+
} else if ('partial_json' in delta) {
|
|
161
|
+
const parts = partialJsonParts.get(event.index);
|
|
162
|
+
if (parts) {
|
|
163
|
+
parts.push(delta.partial_json);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} else if (event.type === 'content_block_stop') {
|
|
167
|
+
const block = contentBlocks[event.index];
|
|
168
|
+
if (block?.type === 'tool_use') {
|
|
169
|
+
const parts = partialJsonParts.get(event.index);
|
|
170
|
+
if (parts && parts.length > 0) {
|
|
171
|
+
try {
|
|
172
|
+
block.input = JSON.parse(parts.join(''));
|
|
173
|
+
} catch {
|
|
174
|
+
block.input = {};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} else if (event.type === 'message_delta') {
|
|
179
|
+
if (finalMessage) {
|
|
180
|
+
finalMessage.stop_reason = event.delta.stop_reason ?? finalMessage.stop_reason;
|
|
181
|
+
if (event.usage) {
|
|
182
|
+
finalMessage.usage.output_tokens = event.usage.output_tokens;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!finalMessage) {
|
|
189
|
+
throw new Error('工具链后续请求未收到响应');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
finalMessage.content = contentBlocks.filter(
|
|
193
|
+
(block): block is Anthropic.TextBlock | Anthropic.ToolUseBlock => block != null
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
return finalMessage;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function handleToolUseInternal(
|
|
200
|
+
response: Anthropic.Message,
|
|
201
|
+
messages: Anthropic.MessageParam[],
|
|
202
|
+
executeTool: ToolExecutor,
|
|
203
|
+
options?: ChatOptions,
|
|
204
|
+
depth: number = 0,
|
|
205
|
+
heartbeat?: HeartbeatCallback
|
|
206
|
+
): Promise<string> {
|
|
207
|
+
if (depth >= MAX_TOOL_CALL_DEPTH) {
|
|
208
|
+
return extractText(response) || `[工具调用链过深(超过 ${MAX_TOOL_CALL_DEPTH} 轮),已强制终止]`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const toolUseBlocks = response.content.filter(
|
|
212
|
+
(block): block is Anthropic.ToolUseBlock => block.type === 'tool_use'
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
if (toolUseBlocks.length === 0) {
|
|
216
|
+
return extractText(response);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let newMessages: Anthropic.MessageParam[] = [
|
|
220
|
+
...messages,
|
|
221
|
+
{
|
|
222
|
+
role: 'assistant' as const,
|
|
223
|
+
content: response.content,
|
|
224
|
+
},
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
const toolResults: Anthropic.ToolResultBlockParam[] = [];
|
|
228
|
+
|
|
229
|
+
for (const toolUse of toolUseBlocks) {
|
|
230
|
+
heartbeat?.();
|
|
231
|
+
try {
|
|
232
|
+
const result = await executeTool(toolUse.name, toolUse.input);
|
|
233
|
+
const content = typeof result === 'string' ? result : JSON.stringify(result);
|
|
234
|
+
toolResults.push({
|
|
235
|
+
type: 'tool_result',
|
|
236
|
+
tool_use_id: toolUse.id,
|
|
237
|
+
content: truncateToolResult(content, MAX_TOOL_RESULT_CHARS),
|
|
238
|
+
});
|
|
239
|
+
} catch (error) {
|
|
240
|
+
toolResults.push({
|
|
241
|
+
type: 'tool_result',
|
|
242
|
+
tool_use_id: toolUse.id,
|
|
243
|
+
content: `工具执行失败: ${(error as Error).message}`,
|
|
244
|
+
is_error: true,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
heartbeat?.();
|
|
250
|
+
|
|
251
|
+
newMessages.push({
|
|
252
|
+
role: 'user',
|
|
253
|
+
content: toolResults,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
if (depth >= KEEP_RECENT_TOOL_ROUNDS) {
|
|
257
|
+
newMessages = compressToolHistory(newMessages, KEEP_RECENT_TOOL_ROUNDS);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const nextResponse = await streamFollowUp(newMessages, options, heartbeat);
|
|
261
|
+
|
|
262
|
+
if (nextResponse.stop_reason === 'tool_use') {
|
|
263
|
+
return handleToolUseInternal(nextResponse, newMessages, executeTool, options, depth + 1, heartbeat);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return extractText(nextResponse);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ==================== Provider 实现 ====================
|
|
270
|
+
|
|
271
|
+
const anthropicProvider: AIProviderPlugin = {
|
|
272
|
+
name: 'anthropic-provider',
|
|
273
|
+
version: '1.0.0',
|
|
274
|
+
description: 'Anthropic AI Provider - 支持 Claude 等模型',
|
|
275
|
+
|
|
276
|
+
async init(config: PluginConfig): Promise<void> {
|
|
277
|
+
const apiKey = config.apiKey as string || process.env.ANTHROPIC_AUTH_TOKEN || process.env.ANTHROPIC_API_KEY;
|
|
278
|
+
if (!apiKey) {
|
|
279
|
+
throw new Error('Missing API key: ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
client = new Anthropic({
|
|
283
|
+
apiKey,
|
|
284
|
+
baseURL: config.baseURL as string || process.env.ANTHROPIC_BASE_URL,
|
|
285
|
+
maxRetries: 0,
|
|
286
|
+
timeout: config.timeout ? Number(config.timeout) : 60000,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
model = config.model as string || process.env.AI_MODEL || process.env.ANTHROPIC_MODEL || 'claude-sonnet-4-20250514';
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
async chat(
|
|
293
|
+
messages: ChatMessage[],
|
|
294
|
+
options?: ChatOptions
|
|
295
|
+
): Promise<Anthropic.Message> {
|
|
296
|
+
if (!client) {
|
|
297
|
+
throw new Error('Provider not initialized. Call init() first.');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const params: Anthropic.MessageCreateParams = {
|
|
301
|
+
model: model,
|
|
302
|
+
max_tokens: options?.maxTokens ?? 4096,
|
|
303
|
+
messages: messages.map(msg => ({
|
|
304
|
+
role: msg.role,
|
|
305
|
+
content: msg.content,
|
|
306
|
+
})),
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
if (options?.system) {
|
|
310
|
+
params.system = options.system;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (options?.tools && options.tools.length > 0) {
|
|
314
|
+
params.tools = options.tools as unknown as Anthropic.Tool[];
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (options?.temperature !== undefined) {
|
|
318
|
+
params.temperature = options.temperature;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (options?.stopSequences && options.stopSequences.length > 0) {
|
|
322
|
+
params.stop_sequences = options.stopSequences;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return await client.messages.create(params);
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
async *chatStream(
|
|
329
|
+
messages: ChatMessage[],
|
|
330
|
+
options?: ChatOptions
|
|
331
|
+
): AsyncGenerator<StreamEvent> {
|
|
332
|
+
if (!client) {
|
|
333
|
+
throw new Error('Provider not initialized. Call init() first.');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const params: Anthropic.MessageCreateParams = {
|
|
337
|
+
model: model,
|
|
338
|
+
max_tokens: options?.maxTokens ?? 4096,
|
|
339
|
+
messages: messages.map(msg => ({
|
|
340
|
+
role: msg.role,
|
|
341
|
+
content: msg.content,
|
|
342
|
+
})),
|
|
343
|
+
stream: true,
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
if (options?.system) {
|
|
347
|
+
params.system = options.system;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (options?.tools && options.tools.length > 0) {
|
|
351
|
+
params.tools = options.tools as unknown as Anthropic.Tool[];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (options?.temperature !== undefined) {
|
|
355
|
+
params.temperature = options.temperature;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const stream = await client.messages.create(params);
|
|
359
|
+
|
|
360
|
+
let finalMessage: Anthropic.Message | null = null;
|
|
361
|
+
const contentBlocks: Array<Anthropic.TextBlock | Anthropic.ToolUseBlock> = [];
|
|
362
|
+
const partialJsonParts = new Map<number, string[]>();
|
|
363
|
+
|
|
364
|
+
for await (const event of stream as AsyncIterable<Anthropic.MessageStreamEvent>) {
|
|
365
|
+
if (event.type === 'message_start') {
|
|
366
|
+
finalMessage = event.message as Anthropic.Message;
|
|
367
|
+
} else if (event.type === 'content_block_start') {
|
|
368
|
+
const block = event.content_block;
|
|
369
|
+
if (block.type === 'text') {
|
|
370
|
+
contentBlocks[event.index] = { type: 'text' as const, text: '', citations: null };
|
|
371
|
+
} else if (block.type === 'tool_use') {
|
|
372
|
+
contentBlocks[event.index] = {
|
|
373
|
+
type: 'tool_use' as const,
|
|
374
|
+
id: block.id,
|
|
375
|
+
name: block.name,
|
|
376
|
+
input: {},
|
|
377
|
+
};
|
|
378
|
+
partialJsonParts.set(event.index, []);
|
|
379
|
+
}
|
|
380
|
+
} else if (event.type === 'content_block_delta') {
|
|
381
|
+
const delta = event.delta;
|
|
382
|
+
if ('text' in delta) {
|
|
383
|
+
const block = contentBlocks[event.index];
|
|
384
|
+
if (block?.type === 'text') {
|
|
385
|
+
block.text += delta.text;
|
|
386
|
+
}
|
|
387
|
+
yield { type: 'text', text: delta.text };
|
|
388
|
+
} else if ('partial_json' in delta) {
|
|
389
|
+
const parts = partialJsonParts.get(event.index);
|
|
390
|
+
if (parts) {
|
|
391
|
+
parts.push(delta.partial_json);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
} else if (event.type === 'content_block_stop') {
|
|
395
|
+
const block = contentBlocks[event.index];
|
|
396
|
+
if (block?.type === 'tool_use') {
|
|
397
|
+
const parts = partialJsonParts.get(event.index);
|
|
398
|
+
if (parts && parts.length > 0) {
|
|
399
|
+
try {
|
|
400
|
+
block.input = JSON.parse(parts.join(''));
|
|
401
|
+
} catch {
|
|
402
|
+
block.input = {};
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
} else if (event.type === 'message_delta') {
|
|
407
|
+
if (finalMessage) {
|
|
408
|
+
finalMessage.stop_reason = event.delta.stop_reason ?? finalMessage.stop_reason;
|
|
409
|
+
if (event.usage) {
|
|
410
|
+
finalMessage.usage.output_tokens = event.usage.output_tokens;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (finalMessage) {
|
|
417
|
+
finalMessage.content = contentBlocks.filter(
|
|
418
|
+
(block): block is Anthropic.TextBlock | Anthropic.ToolUseBlock => block != null
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
for (const block of finalMessage.content) {
|
|
422
|
+
if (block.type === 'tool_use') {
|
|
423
|
+
yield {
|
|
424
|
+
type: 'tool_use',
|
|
425
|
+
id: block.id,
|
|
426
|
+
name: block.name,
|
|
427
|
+
input: block.input,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
yield { type: 'done', message: finalMessage };
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
async handleToolUse(
|
|
437
|
+
response: unknown,
|
|
438
|
+
messages: ChatMessage[],
|
|
439
|
+
executeTool: ToolExecutor,
|
|
440
|
+
options?: ChatOptions,
|
|
441
|
+
heartbeat?: HeartbeatCallback
|
|
442
|
+
): Promise<string> {
|
|
443
|
+
const anthropicResponse = response as Anthropic.Message;
|
|
444
|
+
const apiMessages: Anthropic.MessageParam[] = messages.map(msg => ({
|
|
445
|
+
role: msg.role as 'user' | 'assistant',
|
|
446
|
+
content: msg.content,
|
|
447
|
+
}));
|
|
448
|
+
|
|
449
|
+
return handleToolUseInternal(anthropicResponse, apiMessages, executeTool, options, 0, heartbeat);
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
getModel(): string {
|
|
453
|
+
return model;
|
|
454
|
+
},
|
|
455
|
+
|
|
456
|
+
setModel(newModel: string): void {
|
|
457
|
+
model = newModel;
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
export default anthropicProvider;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI 渠道插件 - 终端交互渠道
|
|
3
|
+
*
|
|
4
|
+
* 作为 FlashClaw 的内置终端渠道,
|
|
5
|
+
* 提供 CLI 命令客户端连接到服务
|
|
6
|
+
*
|
|
7
|
+
* 使用方式:
|
|
8
|
+
* 1. 启动服务: flashclaw start
|
|
9
|
+
* 2. 连接 CLI: flashclaw cli
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ChannelPlugin, MessageHandler, PluginConfig, SendMessageResult } from '../../src/plugins/types.js';
|
|
13
|
+
import { createLogger } from '../../src/logger.js';
|
|
14
|
+
|
|
15
|
+
const logger = createLogger('CLI-Channel');
|
|
16
|
+
|
|
17
|
+
const plugin: ChannelPlugin & {
|
|
18
|
+
group: string;
|
|
19
|
+
} = {
|
|
20
|
+
name: 'cli-channel',
|
|
21
|
+
version: '1.0.0',
|
|
22
|
+
group: 'cli-default',
|
|
23
|
+
|
|
24
|
+
async init(_config: PluginConfig): Promise<void> {
|
|
25
|
+
// CLI 渠道不需要特殊配置
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
onMessage(_handler: MessageHandler): void {
|
|
29
|
+
// CLI 是客户端模式,由 flashclaw cli 命令连接
|
|
30
|
+
// 消息通过 HTTP API 传输
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
async start(): Promise<void> {
|
|
34
|
+
// CLI 渠道是客户端模式,不绑定终端
|
|
35
|
+
// 用户通过 flashclaw cli 命令连接
|
|
36
|
+
// 消息通过 web-ui 的 /api/chat 接口传输
|
|
37
|
+
logger.info('CLI 渠道已就绪,使用 flashclaw cli 连接服务');
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
async stop(): Promise<void> {
|
|
41
|
+
// 清理资源
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
async sendMessage(_chatId: string, content: string): Promise<SendMessageResult> {
|
|
45
|
+
// CLI 渠道的消息由 flashclaw cli 命令处理
|
|
46
|
+
// 这里不需要实现(消息通过 API 传输)
|
|
47
|
+
return { success: true };
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export default plugin;
|