alemonjs-aichat 1.0.35 → 1.0.38
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/lib/api/aitools.js +414 -52
- package/lib/config.js +118 -5
- package/lib/data/help.json.js +8 -0
- package/lib/image/conponent/AiConfig.js +3 -1
- package/lib/middleware/mw.js +3 -0
- package/lib/response/config/res.js +59 -3
- package/lib/response/revoke/res.js +34 -0
- package/lib/response/setting/res.js +16 -0
- package/lib/response/zreply/capi.js +57 -1
- package/lib/response/zreply/getChatConfig.js +39 -36
- package/lib/response/zreply/tools.js +1 -1
- package/lib/routes/commands.js +7 -2
- package/package.json +2 -1
package/lib/api/aitools.js
CHANGED
|
@@ -313,6 +313,36 @@ const tools = [
|
|
|
313
313
|
},
|
|
314
314
|
},
|
|
315
315
|
},
|
|
316
|
+
{
|
|
317
|
+
type: "function",
|
|
318
|
+
function: {
|
|
319
|
+
name: "RunServerCommand",
|
|
320
|
+
description: "直接在服务器上执行运维指令,可用于查看状态、管理服务、Docker、PM2、日志和部署流程。执行前必须通过本地基础拦截和 AI 自动审核;审核不可用或未通过时不会执行。不要用它操作 public/{guid} 项目文件,AI agent 项目文件操作仍使用 Agent* 工具。",
|
|
321
|
+
parameters: {
|
|
322
|
+
type: "object",
|
|
323
|
+
properties: {
|
|
324
|
+
command: {
|
|
325
|
+
type: "string",
|
|
326
|
+
description: "要执行的服务器指令,例如 systemctl status nginx、docker ps、pm2 list、tail -n 100 app.log",
|
|
327
|
+
},
|
|
328
|
+
workingDirectory: {
|
|
329
|
+
type: "string",
|
|
330
|
+
description: "可选。服务器上的执行目录,可以是绝对路径或相对当前进程目录,默认为服务进程当前目录",
|
|
331
|
+
},
|
|
332
|
+
reviewGuid: {
|
|
333
|
+
type: "string",
|
|
334
|
+
description: "必填。传入当前群号,也就是系统提示词里的“当前群号(reviewGuid)”;私聊时也传当前会话的群号/guid。用于读取当前群 AI 配置并执行 AI 自动审核,不是 public/{guid} 项目工作目录ID。",
|
|
335
|
+
},
|
|
336
|
+
timeoutSeconds: {
|
|
337
|
+
type: "number",
|
|
338
|
+
description: "可选。命令超时时间,默认 120 秒,最大 600 秒",
|
|
339
|
+
default: 120,
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
required: ["command", "reviewGuid"],
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
},
|
|
316
346
|
{
|
|
317
347
|
type: "function",
|
|
318
348
|
function: {
|
|
@@ -333,7 +363,12 @@ const tools = [
|
|
|
333
363
|
action: {
|
|
334
364
|
type: "string",
|
|
335
365
|
description: "Git 操作类型:currentBranch 当前分支;listBranches 分支列表;listRemotes 远端列表;switchBranch 切换到已有分支",
|
|
336
|
-
enum: [
|
|
366
|
+
enum: [
|
|
367
|
+
"currentBranch",
|
|
368
|
+
"listBranches",
|
|
369
|
+
"listRemotes",
|
|
370
|
+
"switchBranch",
|
|
371
|
+
],
|
|
337
372
|
},
|
|
338
373
|
branch: {
|
|
339
374
|
type: "string",
|
|
@@ -708,14 +743,14 @@ const tools = [
|
|
|
708
743
|
type: "function",
|
|
709
744
|
function: {
|
|
710
745
|
name: "ping",
|
|
711
|
-
description: `
|
|
712
|
-
检测目标主机是否可达。
|
|
713
|
-
|
|
714
|
-
【限制】
|
|
715
|
-
- 仅允许公网域名
|
|
716
|
-
- 禁止 IP 地址(防止内网扫描)
|
|
717
|
-
- 自动限制次数(例如 3 次)
|
|
718
|
-
|
|
746
|
+
description: `
|
|
747
|
+
检测目标主机是否可达。
|
|
748
|
+
|
|
749
|
+
【限制】
|
|
750
|
+
- 仅允许公网域名
|
|
751
|
+
- 禁止 IP 地址(防止内网扫描)
|
|
752
|
+
- 自动限制次数(例如 3 次)
|
|
753
|
+
|
|
719
754
|
`,
|
|
720
755
|
parameters: {
|
|
721
756
|
type: "object",
|
|
@@ -730,22 +765,22 @@ const tools = [
|
|
|
730
765
|
type: "function",
|
|
731
766
|
function: {
|
|
732
767
|
name: "http_request",
|
|
733
|
-
description: `
|
|
734
|
-
发送 HTTP 请求以获取网页或 API 数据。
|
|
735
|
-
|
|
736
|
-
【能力范围】
|
|
737
|
-
- 支持 GET / POST 请求
|
|
738
|
-
- 返回文本或 JSON 内容
|
|
739
|
-
- 自动处理常见编码
|
|
740
|
-
|
|
741
|
-
【限制】
|
|
742
|
-
- 禁止访问云元数据地址(如 169.254.169.254)
|
|
743
|
-
- 请求超时限制为 5 秒
|
|
744
|
-
- 响应大小限制(例如 1MB)
|
|
745
|
-
|
|
746
|
-
【使用建议】
|
|
747
|
-
- 优先使用此工具获取网页内容,而不是使用 shell
|
|
748
|
-
- 适合抓取网页、调用 API、获取数据
|
|
768
|
+
description: `
|
|
769
|
+
发送 HTTP 请求以获取网页或 API 数据。
|
|
770
|
+
|
|
771
|
+
【能力范围】
|
|
772
|
+
- 支持 GET / POST 请求
|
|
773
|
+
- 返回文本或 JSON 内容
|
|
774
|
+
- 自动处理常见编码
|
|
775
|
+
|
|
776
|
+
【限制】
|
|
777
|
+
- 禁止访问云元数据地址(如 169.254.169.254)
|
|
778
|
+
- 请求超时限制为 5 秒
|
|
779
|
+
- 响应大小限制(例如 1MB)
|
|
780
|
+
|
|
781
|
+
【使用建议】
|
|
782
|
+
- 优先使用此工具获取网页内容,而不是使用 shell
|
|
783
|
+
- 适合抓取网页、调用 API、获取数据
|
|
749
784
|
`,
|
|
750
785
|
parameters: {
|
|
751
786
|
type: "object",
|
|
@@ -830,11 +865,44 @@ const ALLOWED_DOTNET_PROJECT_SUBCOMMANDS = new Set([
|
|
|
830
865
|
"sln",
|
|
831
866
|
]);
|
|
832
867
|
const BLOCKED_PROJECT_COMMAND_PATTERNS = [
|
|
833
|
-
{
|
|
834
|
-
|
|
835
|
-
|
|
868
|
+
{
|
|
869
|
+
pattern: /&&|\|\||[;|<>`]|[$][(]/,
|
|
870
|
+
reason: "不允许使用 shell 控制符、重定向、命令替换或反引号",
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
pattern: /(^|[\s])(rm|del|erase|rmdir|rd)([\s]|$)/i,
|
|
874
|
+
reason: "不允许使用删除命令",
|
|
875
|
+
},
|
|
876
|
+
{
|
|
877
|
+
pattern: /(^|[\s])(powershell|pwsh|cmd|bash|sh|docker|ssh|scp|sftp|curl|wget)([\s]|$)/i,
|
|
878
|
+
reason: "不允许执行受限外部命令",
|
|
879
|
+
},
|
|
836
880
|
{ pattern: /\.\.[\\/]/, reason: "不允许通过 .. 离开 public 工作目录" },
|
|
837
881
|
];
|
|
882
|
+
const SERVER_COMMAND_DEFAULT_TIMEOUT_SECONDS = 120;
|
|
883
|
+
const SERVER_COMMAND_MAX_TIMEOUT_SECONDS = 600;
|
|
884
|
+
const BLOCKED_SERVER_COMMAND_PATTERNS = [
|
|
885
|
+
{
|
|
886
|
+
pattern: /\0/,
|
|
887
|
+
reason: "命令包含非法空字符",
|
|
888
|
+
},
|
|
889
|
+
{
|
|
890
|
+
pattern: /:\s*\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;/,
|
|
891
|
+
reason: "不允许执行 fork bomb",
|
|
892
|
+
},
|
|
893
|
+
{
|
|
894
|
+
pattern: /(^|[\s;&|])rm\s+(-[^\s]*r[^\s]*f|-f[^\s]*r|-[^\s]*rf[^\s]*)\s+(\/|\/\*|~|~\/|\$HOME)(\s|$)/i,
|
|
895
|
+
reason: "不允许递归强制删除系统根目录或用户主目录",
|
|
896
|
+
},
|
|
897
|
+
{
|
|
898
|
+
pattern: /(^|[\s;&|])(mkfs|mkswap|fdisk|parted|wipefs)([\s]|$)/i,
|
|
899
|
+
reason: "不允许执行磁盘格式化或分区破坏类命令",
|
|
900
|
+
},
|
|
901
|
+
{
|
|
902
|
+
pattern: /(^|[\s;&|])dd\s+[\s\S]*\bof=\/dev\//i,
|
|
903
|
+
reason: "不允许使用 dd 写入块设备",
|
|
904
|
+
},
|
|
905
|
+
];
|
|
838
906
|
const toToolError = (error) => ({
|
|
839
907
|
success: false,
|
|
840
908
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -1408,7 +1476,15 @@ const buildProjectCommandReview = (command) => {
|
|
|
1408
1476
|
tokens,
|
|
1409
1477
|
};
|
|
1410
1478
|
}
|
|
1411
|
-
risk = [
|
|
1479
|
+
risk = [
|
|
1480
|
+
"new",
|
|
1481
|
+
"restore",
|
|
1482
|
+
"build",
|
|
1483
|
+
"run",
|
|
1484
|
+
"test",
|
|
1485
|
+
"publish",
|
|
1486
|
+
"clean",
|
|
1487
|
+
].includes(subcommand)
|
|
1412
1488
|
? "medium"
|
|
1413
1489
|
: "low";
|
|
1414
1490
|
reasons.push(risk === "medium"
|
|
@@ -1490,7 +1566,8 @@ const buildProjectCommandReview = (command) => {
|
|
|
1490
1566
|
tokens,
|
|
1491
1567
|
};
|
|
1492
1568
|
}
|
|
1493
|
-
if (["create", "init", "dlx", "exec"].includes(subcommand) ||
|
|
1569
|
+
if (["create", "init", "dlx", "exec"].includes(subcommand) ||
|
|
1570
|
+
["npx", "npx.cmd", "bunx"].includes(baseCommand)) {
|
|
1494
1571
|
risk = "medium";
|
|
1495
1572
|
reasons.push("脚手架或外部执行命令会下载并运行第三方包");
|
|
1496
1573
|
}
|
|
@@ -1498,7 +1575,8 @@ const buildProjectCommandReview = (command) => {
|
|
|
1498
1575
|
risk = "medium";
|
|
1499
1576
|
reasons.push("依赖安装命令会写入项目依赖和锁文件");
|
|
1500
1577
|
}
|
|
1501
|
-
else if (["run", "test", "build"].includes(subcommand) ||
|
|
1578
|
+
else if (["run", "test", "build"].includes(subcommand) ||
|
|
1579
|
+
subcommand === "") {
|
|
1502
1580
|
risk = "low";
|
|
1503
1581
|
reasons.push("项目内脚本或常规包管理命令");
|
|
1504
1582
|
}
|
|
@@ -1514,6 +1592,56 @@ const buildProjectCommandReview = (command) => {
|
|
|
1514
1592
|
tokens,
|
|
1515
1593
|
};
|
|
1516
1594
|
};
|
|
1595
|
+
const buildServerCommandLocalReview = (command) => {
|
|
1596
|
+
const trimmed = command.trim();
|
|
1597
|
+
if (!trimmed) {
|
|
1598
|
+
return {
|
|
1599
|
+
allowed: false,
|
|
1600
|
+
risk: "blocked",
|
|
1601
|
+
reasons: ["命令不能为空"],
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
for (const item of BLOCKED_SERVER_COMMAND_PATTERNS) {
|
|
1605
|
+
if (item.pattern.test(trimmed)) {
|
|
1606
|
+
return {
|
|
1607
|
+
allowed: false,
|
|
1608
|
+
risk: "blocked",
|
|
1609
|
+
reasons: [item.reason],
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
const highRiskPatterns = [
|
|
1614
|
+
/(^|[\s;&|])(sudo|su)([\s]|$)/i,
|
|
1615
|
+
/(^|[\s;&|])(systemctl|service|supervisorctl|pm2)([\s]|$)/i,
|
|
1616
|
+
/(^|[\s;&|])(docker|docker-compose|kubectl)([\s]|$)/i,
|
|
1617
|
+
/(^|[\s;&|])(apt|apt-get|yum|dnf|pacman|apk)([\s]|$)/i,
|
|
1618
|
+
/(^|[\s;&|])(iptables|ufw|firewall-cmd)([\s]|$)/i,
|
|
1619
|
+
/(^|[\s;&|])(rm|mv|cp|chmod|chown|kill|pkill)([\s]|$)/i,
|
|
1620
|
+
];
|
|
1621
|
+
const mediumRiskPatterns = [
|
|
1622
|
+
/(^|[\s;&|])(curl|wget|git|tar|unzip|npm|pnpm|yarn|node)([\s]|$)/i,
|
|
1623
|
+
/[;&|<>`]|[$][(]/,
|
|
1624
|
+
];
|
|
1625
|
+
if (highRiskPatterns.some((pattern) => pattern.test(trimmed))) {
|
|
1626
|
+
return {
|
|
1627
|
+
allowed: true,
|
|
1628
|
+
risk: "high",
|
|
1629
|
+
reasons: ["服务器管理或文件/进程/网络配置相关命令,必须经过 AI 审核"],
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
if (mediumRiskPatterns.some((pattern) => pattern.test(trimmed))) {
|
|
1633
|
+
return {
|
|
1634
|
+
allowed: true,
|
|
1635
|
+
risk: "medium",
|
|
1636
|
+
reasons: ["可能修改服务状态、下载内容或使用 shell 控制符,必须经过 AI 审核"],
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
return {
|
|
1640
|
+
allowed: true,
|
|
1641
|
+
risk: "low",
|
|
1642
|
+
reasons: ["常规服务器查询或低风险命令,仍需 AI 审核确认"],
|
|
1643
|
+
};
|
|
1644
|
+
};
|
|
1517
1645
|
const getReviewAIConfig = async (reviewGuid) => {
|
|
1518
1646
|
const guid = reviewGuid?.trim();
|
|
1519
1647
|
const config = await redisClient.getAIConfig(guid || undefined);
|
|
@@ -1548,8 +1676,8 @@ const reviewProjectCommandWithAI = async ({ command, localReview, reviewGuid, })
|
|
|
1548
1676
|
messages: [
|
|
1549
1677
|
{
|
|
1550
1678
|
role: "system",
|
|
1551
|
-
content:
|
|
1552
|
-
|
|
1679
|
+
content: '你是一个命令安全审查器。请只输出 JSON,格式为 {"allowed":boolean,"risk":"low|medium|high|blocked","reason":"简短中文原因"}。审核标准:命令必须只适用于 public 工作目录内的安全项目操作,不得包含权限提升、删除系统文件、远程控制、数据外传、全局环境修改或危险 shell 行为。' +
|
|
1680
|
+
`\n性格约束:${aiConfig.config.systemPrompt || ""}`,
|
|
1553
1681
|
},
|
|
1554
1682
|
{
|
|
1555
1683
|
role: "user",
|
|
@@ -1598,6 +1726,143 @@ const reviewProjectCommandWithAI = async ({ command, localReview, reviewGuid, })
|
|
|
1598
1726
|
};
|
|
1599
1727
|
}
|
|
1600
1728
|
};
|
|
1729
|
+
const reviewServerCommandWithAI = async ({ command, localReview, reviewGuid, workingDirectory, }) => {
|
|
1730
|
+
const aiConfig = await getReviewAIConfig(reviewGuid);
|
|
1731
|
+
if (!aiConfig) {
|
|
1732
|
+
return {
|
|
1733
|
+
status: "skipped",
|
|
1734
|
+
reason: "未找到可用的当前 AI 配置,服务器指令必须完成 AI 审核后才能执行",
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
try {
|
|
1738
|
+
const openai = new OpenAi({
|
|
1739
|
+
baseURL: aiConfig.config.host,
|
|
1740
|
+
apiKey: aiConfig.config.key,
|
|
1741
|
+
timeout: 30000,
|
|
1742
|
+
});
|
|
1743
|
+
const response = await openai.chat.completions.create({
|
|
1744
|
+
model: aiConfig.config.model,
|
|
1745
|
+
temperature: 0,
|
|
1746
|
+
max_tokens: 220,
|
|
1747
|
+
messages: [
|
|
1748
|
+
{
|
|
1749
|
+
role: "system",
|
|
1750
|
+
content: '你是服务器运维指令安全审核器。请只输出 JSON,格式为 {"allowed":boolean,"risk":"low|medium|high|blocked","reason":"简短中文原因"}。可以允许合理的服务器管理、状态查看、日志查看、服务重启、Docker/PM2/systemctl 操作、部署和包管理命令;必须阻止清空数据、破坏系统、植入后门、泄露密钥、反弹 shell、未知脚本下载后直接执行、越权访问或明显不可恢复的危险操作。' +
|
|
1751
|
+
`\n性格约束:${aiConfig.config.systemPrompt || ""}`,
|
|
1752
|
+
},
|
|
1753
|
+
{
|
|
1754
|
+
role: "user",
|
|
1755
|
+
content: JSON.stringify({
|
|
1756
|
+
command,
|
|
1757
|
+
workingDirectory,
|
|
1758
|
+
localReview: {
|
|
1759
|
+
allowed: localReview.allowed,
|
|
1760
|
+
risk: localReview.risk,
|
|
1761
|
+
reasons: localReview.reasons,
|
|
1762
|
+
},
|
|
1763
|
+
}),
|
|
1764
|
+
},
|
|
1765
|
+
],
|
|
1766
|
+
});
|
|
1767
|
+
const text = response.choices[0]?.message?.content || "";
|
|
1768
|
+
const match = text.match(/\{[\s\S]*\}/);
|
|
1769
|
+
if (!match) {
|
|
1770
|
+
return {
|
|
1771
|
+
status: "failed",
|
|
1772
|
+
reason: "AI 审核未返回可解析的 JSON",
|
|
1773
|
+
raw: text,
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
const parsed = JSON.parse(match[0]);
|
|
1777
|
+
return {
|
|
1778
|
+
status: "completed",
|
|
1779
|
+
currentAI: aiConfig.currentAI || null,
|
|
1780
|
+
model: aiConfig.config.model,
|
|
1781
|
+
guid: aiConfig.guid,
|
|
1782
|
+
allowed: Boolean(parsed.allowed),
|
|
1783
|
+
risk: parsed.risk === "low" ||
|
|
1784
|
+
parsed.risk === "medium" ||
|
|
1785
|
+
parsed.risk === "high" ||
|
|
1786
|
+
parsed.risk === "blocked"
|
|
1787
|
+
? parsed.risk
|
|
1788
|
+
: "high",
|
|
1789
|
+
reason: typeof parsed.reason === "string" && parsed.reason.trim()
|
|
1790
|
+
? parsed.reason.trim()
|
|
1791
|
+
: "AI 未提供明确原因",
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
catch (error) {
|
|
1795
|
+
return {
|
|
1796
|
+
status: "failed",
|
|
1797
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
};
|
|
1801
|
+
const resolveServerWorkingDirectory = (workingDirectory) => {
|
|
1802
|
+
if (workingDirectory?.includes("\0")) {
|
|
1803
|
+
throw new Error("workingDirectory 包含非法空字符");
|
|
1804
|
+
}
|
|
1805
|
+
const cwd = path.resolve(workingDirectory?.trim() || process.cwd());
|
|
1806
|
+
if (!fs.existsSync(cwd)) {
|
|
1807
|
+
throw new Error("workingDirectory 不存在");
|
|
1808
|
+
}
|
|
1809
|
+
if (!fs.statSync(cwd).isDirectory()) {
|
|
1810
|
+
throw new Error("workingDirectory 不是目录");
|
|
1811
|
+
}
|
|
1812
|
+
return cwd;
|
|
1813
|
+
};
|
|
1814
|
+
const normalizeServerCommandTimeoutMs = (timeoutSeconds) => {
|
|
1815
|
+
const safeSeconds = Math.min(normalizePositiveInteger(timeoutSeconds, SERVER_COMMAND_DEFAULT_TIMEOUT_SECONDS, "timeoutSeconds"), SERVER_COMMAND_MAX_TIMEOUT_SECONDS);
|
|
1816
|
+
return safeSeconds * 1000;
|
|
1817
|
+
};
|
|
1818
|
+
const runServerShellCommand = ({ command, cwd, timeoutMs, }) => {
|
|
1819
|
+
return new Promise((resolve, reject) => {
|
|
1820
|
+
const powershellCommand = [
|
|
1821
|
+
"chcp 65001 | Out-Null",
|
|
1822
|
+
"[Console]::InputEncoding = [System.Text.UTF8Encoding]::new()",
|
|
1823
|
+
"[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()",
|
|
1824
|
+
"$OutputEncoding = [System.Text.UTF8Encoding]::new()",
|
|
1825
|
+
command,
|
|
1826
|
+
].join("; ");
|
|
1827
|
+
const shell = process.platform === "win32"
|
|
1828
|
+
? {
|
|
1829
|
+
file: "powershell.exe",
|
|
1830
|
+
args: [
|
|
1831
|
+
"-NoProfile",
|
|
1832
|
+
"-ExecutionPolicy",
|
|
1833
|
+
"Bypass",
|
|
1834
|
+
"-Command",
|
|
1835
|
+
powershellCommand,
|
|
1836
|
+
],
|
|
1837
|
+
}
|
|
1838
|
+
: { file: "sh", args: ["-lc", command] };
|
|
1839
|
+
const child = spawn(shell.file, shell.args, { cwd });
|
|
1840
|
+
let output = "";
|
|
1841
|
+
const appendOutput = (data) => {
|
|
1842
|
+
output += data.toString();
|
|
1843
|
+
if (output.length > PROJECT_COMMAND_MAX_OUTPUT) {
|
|
1844
|
+
output = output.slice(-PROJECT_COMMAND_MAX_OUTPUT);
|
|
1845
|
+
}
|
|
1846
|
+
};
|
|
1847
|
+
const timeout = setTimeout(() => {
|
|
1848
|
+
child.kill();
|
|
1849
|
+
reject(new Error("服务器指令执行超时"));
|
|
1850
|
+
}, timeoutMs);
|
|
1851
|
+
child.stdout.on("data", appendOutput);
|
|
1852
|
+
child.stderr.on("data", appendOutput);
|
|
1853
|
+
child.on("error", (error) => {
|
|
1854
|
+
clearTimeout(timeout);
|
|
1855
|
+
reject(new Error(`服务器指令启动失败: ${error.message}`));
|
|
1856
|
+
});
|
|
1857
|
+
child.on("close", (exitCode) => {
|
|
1858
|
+
clearTimeout(timeout);
|
|
1859
|
+
resolve({
|
|
1860
|
+
output: trimProjectCommandOutput(output.trim()),
|
|
1861
|
+
exitCode: exitCode ?? -1,
|
|
1862
|
+
});
|
|
1863
|
+
});
|
|
1864
|
+
});
|
|
1865
|
+
};
|
|
1601
1866
|
const runSpawnCommand = ({ file, args, cwd, }) => {
|
|
1602
1867
|
return new Promise((resolve, reject) => {
|
|
1603
1868
|
const child = spawn(file, args, { cwd });
|
|
@@ -1662,14 +1927,22 @@ const findExecutablePaths = async (name, cwd) => {
|
|
|
1662
1927
|
};
|
|
1663
1928
|
const inspectProjectEnvironment = async (cwd) => {
|
|
1664
1929
|
const versionCommands = [
|
|
1665
|
-
{
|
|
1930
|
+
{
|
|
1931
|
+
name: "dotnet",
|
|
1932
|
+
file: process.platform === "win32" ? "dotnet.exe" : "dotnet",
|
|
1933
|
+
args: ["--version"],
|
|
1934
|
+
},
|
|
1666
1935
|
process.platform === "win32"
|
|
1667
1936
|
? { name: "npm", file: "npm.cmd", args: ["-v"] }
|
|
1668
1937
|
: { name: "npm", file: "npm", args: ["-v"] },
|
|
1669
1938
|
process.platform === "win32"
|
|
1670
1939
|
? { name: "npx", file: "npx.cmd", args: ["-v"] }
|
|
1671
1940
|
: { name: "npx", file: "npx", args: ["-v"] },
|
|
1672
|
-
{
|
|
1941
|
+
{
|
|
1942
|
+
name: "node",
|
|
1943
|
+
file: process.platform === "win32" ? "node.exe" : "node",
|
|
1944
|
+
args: ["-v"],
|
|
1945
|
+
},
|
|
1673
1946
|
{ name: "git", file: "git", args: ["--version"] },
|
|
1674
1947
|
];
|
|
1675
1948
|
const versions = {};
|
|
@@ -1682,11 +1955,15 @@ const inspectProjectEnvironment = async (cwd) => {
|
|
|
1682
1955
|
cwd,
|
|
1683
1956
|
});
|
|
1684
1957
|
versions[command.name] =
|
|
1685
|
-
result.exitCode === 0
|
|
1958
|
+
result.exitCode === 0
|
|
1959
|
+
? result.output || "ok"
|
|
1960
|
+
: `exit ${result.exitCode}`;
|
|
1686
1961
|
}
|
|
1687
1962
|
catch (error) {
|
|
1688
1963
|
versions[command.name] =
|
|
1689
|
-
error instanceof Error
|
|
1964
|
+
error instanceof Error
|
|
1965
|
+
? `unavailable: ${error.message}`
|
|
1966
|
+
: String(error);
|
|
1690
1967
|
}
|
|
1691
1968
|
paths[command.name] = await findExecutablePaths(command.name, cwd);
|
|
1692
1969
|
}
|
|
@@ -1694,7 +1971,10 @@ const inspectProjectEnvironment = async (cwd) => {
|
|
|
1694
1971
|
cwd,
|
|
1695
1972
|
versions,
|
|
1696
1973
|
paths,
|
|
1697
|
-
entries: fs
|
|
1974
|
+
entries: fs
|
|
1975
|
+
.readdirSync(cwd, { withFileTypes: true })
|
|
1976
|
+
.slice(0, 50)
|
|
1977
|
+
.map((item) => ({
|
|
1698
1978
|
name: item.name,
|
|
1699
1979
|
type: item.isDirectory() ? "directory" : "file",
|
|
1700
1980
|
})),
|
|
@@ -1722,7 +2002,9 @@ const extractArchiveWithinWorkspace = async ({ archive, destination, }) => {
|
|
|
1722
2002
|
cwd: destination,
|
|
1723
2003
|
});
|
|
1724
2004
|
}
|
|
1725
|
-
if (lower.endsWith(".tar") ||
|
|
2005
|
+
if (lower.endsWith(".tar") ||
|
|
2006
|
+
lower.endsWith(".tar.gz") ||
|
|
2007
|
+
lower.endsWith(".tgz")) {
|
|
1726
2008
|
return runSpawnCommand({
|
|
1727
2009
|
file: "tar",
|
|
1728
2010
|
args: ["-xf", archive, "-C", destination],
|
|
@@ -1750,15 +2032,50 @@ const performProjectCommandReview = async ({ command, reviewGuid, }) => {
|
|
|
1750
2032
|
aiReview,
|
|
1751
2033
|
};
|
|
1752
2034
|
};
|
|
2035
|
+
const performServerCommandReview = async ({ command, reviewGuid, workingDirectory, }) => {
|
|
2036
|
+
const localReview = buildServerCommandLocalReview(command);
|
|
2037
|
+
if (!localReview.allowed) {
|
|
2038
|
+
return {
|
|
2039
|
+
allowed: false,
|
|
2040
|
+
risk: localReview.risk,
|
|
2041
|
+
localReview,
|
|
2042
|
+
aiReview: {
|
|
2043
|
+
status: "skipped",
|
|
2044
|
+
reason: "本地基础拦截未通过,未继续调用 AI 审核",
|
|
2045
|
+
},
|
|
2046
|
+
};
|
|
2047
|
+
}
|
|
2048
|
+
const aiReview = await reviewServerCommandWithAI({
|
|
2049
|
+
command,
|
|
2050
|
+
localReview,
|
|
2051
|
+
reviewGuid,
|
|
2052
|
+
workingDirectory,
|
|
2053
|
+
});
|
|
2054
|
+
const allowed = aiReview.status === "completed" && aiReview.allowed === true;
|
|
2055
|
+
const risk = aiReview.status === "completed" && aiReview.risk
|
|
2056
|
+
? aiReview.risk
|
|
2057
|
+
: localReview.risk;
|
|
2058
|
+
return {
|
|
2059
|
+
allowed,
|
|
2060
|
+
risk,
|
|
2061
|
+
localReview,
|
|
2062
|
+
aiReview,
|
|
2063
|
+
};
|
|
2064
|
+
};
|
|
1753
2065
|
/**
|
|
1754
2066
|
* 确保 Docker 容器已创建并运行
|
|
1755
2067
|
*/
|
|
1756
2068
|
async function ensureContainerRunning(config) {
|
|
1757
|
-
const { containerName, image, memory, cpus, pidsLimit, networkMode, workspacePath } = config;
|
|
2069
|
+
const { containerName, image, memory, cpus, pidsLimit, networkMode, workspacePath, } = config;
|
|
1758
2070
|
if (!containerName)
|
|
1759
2071
|
return;
|
|
1760
2072
|
return new Promise((resolve, reject) => {
|
|
1761
|
-
const check = spawn("docker", [
|
|
2073
|
+
const check = spawn("docker", [
|
|
2074
|
+
"inspect",
|
|
2075
|
+
"-f",
|
|
2076
|
+
"{{.State.Running}}",
|
|
2077
|
+
containerName,
|
|
2078
|
+
]);
|
|
1762
2079
|
let stdout = "";
|
|
1763
2080
|
check.stdout.on("data", (d) => (stdout += d.toString()));
|
|
1764
2081
|
check.on("error", (error) => {
|
|
@@ -1787,12 +2104,18 @@ async function ensureContainerRunning(config) {
|
|
|
1787
2104
|
return;
|
|
1788
2105
|
}
|
|
1789
2106
|
const args = [
|
|
1790
|
-
"run",
|
|
1791
|
-
"
|
|
1792
|
-
"--
|
|
1793
|
-
|
|
1794
|
-
"--
|
|
1795
|
-
|
|
2107
|
+
"run",
|
|
2108
|
+
"-d",
|
|
2109
|
+
"--name",
|
|
2110
|
+
containerName,
|
|
2111
|
+
"--memory",
|
|
2112
|
+
memory || "256m",
|
|
2113
|
+
"--cpus",
|
|
2114
|
+
cpus || "0.5",
|
|
2115
|
+
"--pids-limit",
|
|
2116
|
+
String(pidsLimit || 64),
|
|
2117
|
+
"--network",
|
|
2118
|
+
networkMode || "none",
|
|
1796
2119
|
];
|
|
1797
2120
|
if (workspacePath && fs.existsSync(workspacePath)) {
|
|
1798
2121
|
args.push("-v", `${workspacePath}:/workspace`, "-w", "/workspace");
|
|
@@ -2224,7 +2547,7 @@ const availableTools = {
|
|
|
2224
2547
|
* @param command 命令字符串
|
|
2225
2548
|
* @returns 命令执行结果
|
|
2226
2549
|
*/
|
|
2227
|
-
exec: async ({ userId, groupId, command }) => {
|
|
2550
|
+
exec: async ({ userId, groupId, command, }) => {
|
|
2228
2551
|
const workspace = getWorkspace(userId);
|
|
2229
2552
|
if (commandTargetsAgentWorkspace(command || "")) {
|
|
2230
2553
|
return [
|
|
@@ -2385,6 +2708,46 @@ const availableTools = {
|
|
|
2385
2708
|
return toToolError(error);
|
|
2386
2709
|
}
|
|
2387
2710
|
},
|
|
2711
|
+
RunServerCommand: async ({ command, workingDirectory, reviewGuid, timeoutSeconds = SERVER_COMMAND_DEFAULT_TIMEOUT_SECONDS, }) => {
|
|
2712
|
+
try {
|
|
2713
|
+
if (!reviewGuid?.trim()) {
|
|
2714
|
+
throw new Error("reviewGuid 必填,请传入当前群号");
|
|
2715
|
+
}
|
|
2716
|
+
const cwd = resolveServerWorkingDirectory(workingDirectory);
|
|
2717
|
+
const timeoutMs = normalizeServerCommandTimeoutMs(timeoutSeconds);
|
|
2718
|
+
const review = await performServerCommandReview({
|
|
2719
|
+
command,
|
|
2720
|
+
reviewGuid,
|
|
2721
|
+
workingDirectory: cwd,
|
|
2722
|
+
});
|
|
2723
|
+
if (!review.allowed) {
|
|
2724
|
+
return {
|
|
2725
|
+
success: false,
|
|
2726
|
+
command,
|
|
2727
|
+
workingDirectory: cwd,
|
|
2728
|
+
error: "服务器指令未通过 AI 自动审核",
|
|
2729
|
+
review,
|
|
2730
|
+
};
|
|
2731
|
+
}
|
|
2732
|
+
const result = await runServerShellCommand({
|
|
2733
|
+
command,
|
|
2734
|
+
cwd,
|
|
2735
|
+
timeoutMs,
|
|
2736
|
+
});
|
|
2737
|
+
return {
|
|
2738
|
+
success: result.exitCode === 0,
|
|
2739
|
+
command,
|
|
2740
|
+
workingDirectory: cwd,
|
|
2741
|
+
review,
|
|
2742
|
+
output: result.output,
|
|
2743
|
+
exitCode: result.exitCode,
|
|
2744
|
+
error: result.exitCode === 0 ? undefined : "服务器指令执行失败",
|
|
2745
|
+
};
|
|
2746
|
+
}
|
|
2747
|
+
catch (error) {
|
|
2748
|
+
return toToolError(error);
|
|
2749
|
+
}
|
|
2750
|
+
},
|
|
2388
2751
|
AgentGitOperation: async ({ guid, action, workingDirectory = ".", branch, includeRemote = true, }) => {
|
|
2389
2752
|
try {
|
|
2390
2753
|
const { target: cwd, relativePath } = resolveAgentPath(guid, workingDirectory);
|
|
@@ -2494,7 +2857,8 @@ const availableTools = {
|
|
|
2494
2857
|
throw new Error("action=extract 时必须提供 archivePath");
|
|
2495
2858
|
}
|
|
2496
2859
|
const archive = resolveAgentPath(guid, archivePath);
|
|
2497
|
-
if (!fs.existsSync(archive.target) ||
|
|
2860
|
+
if (!fs.existsSync(archive.target) ||
|
|
2861
|
+
!fs.statSync(archive.target).isFile()) {
|
|
2498
2862
|
throw new Error("压缩包不存在或不是文件");
|
|
2499
2863
|
}
|
|
2500
2864
|
const destination = resolveAgentPath(guid, destinationPath || workingDirectory || ".");
|
|
@@ -2757,9 +3121,7 @@ const availableTools = {
|
|
|
2757
3121
|
fileName: fileName || null,
|
|
2758
3122
|
uploadType,
|
|
2759
3123
|
url,
|
|
2760
|
-
sendText: uploadType === "image"
|
|
2761
|
-
? `<img=${url}>`
|
|
2762
|
-
: `文件链接: ${url}`,
|
|
3124
|
+
sendText: uploadType === "image" ? `<img=${url}>` : `文件链接: ${url}`,
|
|
2763
3125
|
};
|
|
2764
3126
|
}
|
|
2765
3127
|
catch (error) {
|
package/lib/config.js
CHANGED
|
@@ -3,6 +3,8 @@ import fs from 'fs';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { redis } from './redis.js';
|
|
5
5
|
|
|
6
|
+
const TOOL_LOOP_TTL_SECONDS = 600;
|
|
7
|
+
const TOOL_LOOP_STALE_MS = TOOL_LOOP_TTL_SECONDS * 1000;
|
|
6
8
|
class db {
|
|
7
9
|
redis;
|
|
8
10
|
systemPrompt = "";
|
|
@@ -237,6 +239,14 @@ class db {
|
|
|
237
239
|
async setToolPromptArgsSwitch(guid, enable) {
|
|
238
240
|
await this.redis.set(`ai:tool_prompt_args:switch:${guid}`, enable ? "1" : "0");
|
|
239
241
|
}
|
|
242
|
+
/** 工具调用前是否发送模型返回的 content */
|
|
243
|
+
async getToolCallContentSwitch(guid) {
|
|
244
|
+
return (await this.redis.get(`ai:tool_call_content:switch:${guid}`)) || "0";
|
|
245
|
+
}
|
|
246
|
+
/** 设置工具调用前 content 发送开关状态 */
|
|
247
|
+
async setToolCallContentSwitch(guid, enable) {
|
|
248
|
+
await this.redis.set(`ai:tool_call_content:switch:${guid}`, enable ? "1" : "0");
|
|
249
|
+
}
|
|
240
250
|
/** --------------------------------------------------------- */
|
|
241
251
|
/** 获取复杂输出开关状态 */
|
|
242
252
|
async getComplexOutput(guid) {
|
|
@@ -427,9 +437,50 @@ class db {
|
|
|
427
437
|
for (const key of keys) {
|
|
428
438
|
await this.redis.del(key);
|
|
429
439
|
}
|
|
440
|
+
const contextStatsKeys = await this.redis.keys(`ai:context_stats:*`);
|
|
441
|
+
if (contextStatsKeys.length > 0) {
|
|
442
|
+
await this.redis.del(...contextStatsKeys);
|
|
443
|
+
}
|
|
444
|
+
await this.clearAITransientState();
|
|
430
445
|
return;
|
|
431
446
|
}
|
|
432
|
-
await this.
|
|
447
|
+
await this.clearAITransientState(guid);
|
|
448
|
+
await this.redis.del(`ai:history:${guid}`, `ai:context_stats:${guid}`);
|
|
449
|
+
}
|
|
450
|
+
/** 记录当前会话上下文和 token 使用情况 */
|
|
451
|
+
async recordAIContextStats(guid, input) {
|
|
452
|
+
const current = await this.getAIContextStats(guid);
|
|
453
|
+
const promptTokens = input.promptTokens ?? 0;
|
|
454
|
+
const completionTokens = input.completionTokens ?? 0;
|
|
455
|
+
const totalTokens = input.totalTokens ?? promptTokens + completionTokens;
|
|
456
|
+
const cachedTokens = input.cachedTokens ?? 0;
|
|
457
|
+
const cacheMissTokens = input.cacheMissTokens ?? Math.max(promptTokens - cachedTokens, 0);
|
|
458
|
+
const nextStats = {
|
|
459
|
+
...input,
|
|
460
|
+
promptTokens: input.promptTokens ?? null,
|
|
461
|
+
completionTokens: input.completionTokens ?? null,
|
|
462
|
+
totalTokens: input.totalTokens ?? null,
|
|
463
|
+
cachedTokens: input.cachedTokens ?? null,
|
|
464
|
+
cacheMissTokens: input.cacheMissTokens ?? null,
|
|
465
|
+
guid,
|
|
466
|
+
requestCount: (current?.requestCount ?? 0) + 1,
|
|
467
|
+
updatedAt: Date.now(),
|
|
468
|
+
totalPromptTokens: (current?.totalPromptTokens ?? 0) + promptTokens,
|
|
469
|
+
totalCompletionTokens: (current?.totalCompletionTokens ?? 0) + completionTokens,
|
|
470
|
+
totalTokensUsed: (current?.totalTokensUsed ?? 0) + totalTokens,
|
|
471
|
+
totalCachedTokens: (current?.totalCachedTokens ?? 0) + cachedTokens,
|
|
472
|
+
totalCacheMissTokens: (current?.totalCacheMissTokens ?? 0) + cacheMissTokens,
|
|
473
|
+
};
|
|
474
|
+
await this.redis.set(`ai:context_stats:${guid}`, JSON.stringify(nextStats));
|
|
475
|
+
return nextStats;
|
|
476
|
+
}
|
|
477
|
+
/** 获取当前会话上下文和 token 使用情况 */
|
|
478
|
+
async getAIContextStats(guid) {
|
|
479
|
+
const statsStr = await this.redis.get(`ai:context_stats:${guid}`);
|
|
480
|
+
if (!statsStr) {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
return JSON.parse(statsStr);
|
|
433
484
|
}
|
|
434
485
|
/** 清理最后的N条聊天记录 */
|
|
435
486
|
async clearLastNChatHistory(guid, n) {
|
|
@@ -464,17 +515,72 @@ class db {
|
|
|
464
515
|
await this.redis.set(this.getArchiveRecordKey(guid, id), JSON.stringify(record));
|
|
465
516
|
return record;
|
|
466
517
|
}
|
|
518
|
+
/** 清理工具循环等临时会话状态 */
|
|
519
|
+
async clearAITransientState(guid) {
|
|
520
|
+
if (!guid) {
|
|
521
|
+
const transientPrefixes = [
|
|
522
|
+
"ai:tool_loop_processing:",
|
|
523
|
+
"ai:guidance:",
|
|
524
|
+
"ai:session:",
|
|
525
|
+
];
|
|
526
|
+
for (const prefix of transientPrefixes) {
|
|
527
|
+
const keys = await this.redis.keys(`${prefix}*`);
|
|
528
|
+
if (keys.length > 0) {
|
|
529
|
+
await this.redis.del(...keys);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
await this.redis.del(`ai:tool_loop_processing:${guid}`, `ai:guidance:${guid}`, `ai:session:${guid}`);
|
|
535
|
+
}
|
|
467
536
|
/** 设置工具调用中的处理状态 */
|
|
468
537
|
async setAIToolLoopProcessing(guid, processing) {
|
|
469
538
|
if (processing) {
|
|
470
|
-
|
|
539
|
+
const now = Date.now();
|
|
540
|
+
const state = {
|
|
541
|
+
processing: true,
|
|
542
|
+
startedAt: now,
|
|
543
|
+
updatedAt: now,
|
|
544
|
+
};
|
|
545
|
+
await this.redis.set(`ai:tool_loop_processing:${guid}`, JSON.stringify(state), "EX", TOOL_LOOP_TTL_SECONDS);
|
|
471
546
|
return;
|
|
472
547
|
}
|
|
473
548
|
await this.redis.del(`ai:tool_loop_processing:${guid}`);
|
|
474
549
|
}
|
|
475
550
|
/** 获取工具调用中的处理状态 */
|
|
476
551
|
async getAIToolLoopProcessing(guid) {
|
|
477
|
-
|
|
552
|
+
const key = `ai:tool_loop_processing:${guid}`;
|
|
553
|
+
const processing = await this.redis.get(key);
|
|
554
|
+
if (!processing) {
|
|
555
|
+
return "0";
|
|
556
|
+
}
|
|
557
|
+
let state = null;
|
|
558
|
+
if (processing !== "1") {
|
|
559
|
+
try {
|
|
560
|
+
const parsed = JSON.parse(processing);
|
|
561
|
+
if (parsed?.processing === true) {
|
|
562
|
+
state = parsed;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
catch {
|
|
566
|
+
await this.clearAITransientState(guid);
|
|
567
|
+
return "0";
|
|
568
|
+
}
|
|
569
|
+
const updatedAt = Number(state?.updatedAt || state?.startedAt || 0);
|
|
570
|
+
if (!updatedAt || Date.now() - updatedAt > TOOL_LOOP_STALE_MS) {
|
|
571
|
+
await this.clearAITransientState(guid);
|
|
572
|
+
return "0";
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const ttl = await this.redis.ttl(key);
|
|
576
|
+
if (ttl === -1) {
|
|
577
|
+
if (processing === "1") {
|
|
578
|
+
await this.clearAITransientState(guid);
|
|
579
|
+
return "0";
|
|
580
|
+
}
|
|
581
|
+
await this.redis.expire(key, TOOL_LOOP_TTL_SECONDS);
|
|
582
|
+
}
|
|
583
|
+
return "1";
|
|
478
584
|
}
|
|
479
585
|
/** 获取工具调用期间追加的引导消息 */
|
|
480
586
|
async getAIGuidanceMessages(guid) {
|
|
@@ -482,13 +588,20 @@ class db {
|
|
|
482
588
|
if (!queueStr) {
|
|
483
589
|
return [];
|
|
484
590
|
}
|
|
485
|
-
|
|
591
|
+
try {
|
|
592
|
+
const queue = JSON.parse(queueStr);
|
|
593
|
+
return Array.isArray(queue) ? queue : [];
|
|
594
|
+
}
|
|
595
|
+
catch {
|
|
596
|
+
await this.clearAIGuidanceMessages(guid);
|
|
597
|
+
return [];
|
|
598
|
+
}
|
|
486
599
|
}
|
|
487
600
|
/** 追加工具调用期间的引导消息 */
|
|
488
601
|
async addAIGuidanceMessage(guid, data) {
|
|
489
602
|
const queue = await this.getAIGuidanceMessages(guid);
|
|
490
603
|
queue.push(data);
|
|
491
|
-
await this.redis.set(`ai:guidance:${guid}`, JSON.stringify(queue));
|
|
604
|
+
await this.redis.set(`ai:guidance:${guid}`, JSON.stringify(queue), "EX", TOOL_LOOP_TTL_SECONDS);
|
|
492
605
|
return queue;
|
|
493
606
|
}
|
|
494
607
|
/** 提取并清空工具调用期间的引导消息 */
|
package/lib/data/help.json.js
CHANGED
|
@@ -136,6 +136,10 @@ var list = [
|
|
|
136
136
|
{
|
|
137
137
|
cmd: "/清空对话",
|
|
138
138
|
desc: "清空当前页面的对话记录。"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
cmd: "/撤回",
|
|
142
|
+
desc: "引用一条消息发送撤回指令,自动撤回被引用的消息。"
|
|
139
143
|
}
|
|
140
144
|
]
|
|
141
145
|
}
|
|
@@ -200,6 +204,10 @@ var list = [
|
|
|
200
204
|
{
|
|
201
205
|
cmd: "/[开启|关闭]工具提示详情",
|
|
202
206
|
desc: "开启或关闭工具调用提示详情功能。开启后工具调用的提示消息会包含更多的详情信息。"
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
cmd: "/[开启|关闭]工具调用内容",
|
|
210
|
+
desc: "开启或关闭工具调用前模型返回内容的发送。开启后会发送工具调用响应中 content 的内容。"
|
|
203
211
|
}
|
|
204
212
|
]
|
|
205
213
|
}
|
|
@@ -9,6 +9,7 @@ const cmds = [
|
|
|
9
9
|
{ cmd: "/切换模型 <模型名称>", desc: "切换当前使用的AI模型" },
|
|
10
10
|
{ cmd: "/<开启|关闭>仅艾特触发", desc: "开启或关闭仅艾特触发功能" },
|
|
11
11
|
{ cmd: "/<开启|关闭>工具", desc: "开启或关闭AI工具" },
|
|
12
|
+
{ cmd: "/<开启|关闭>工具调用内容", desc: "开启或关闭工具调用前内容发送" },
|
|
12
13
|
{ cmd: "/<开启|关闭>复杂输出", desc: "开启或关闭复杂输出" },
|
|
13
14
|
{ cmd: "/<开启|关闭>好感度", desc: "开启或关闭好感度系统" },
|
|
14
15
|
{ cmd: "/清空对话", desc: "清空当前对话历史" },
|
|
@@ -76,7 +77,8 @@ function App(data) {
|
|
|
76
77
|
React.createElement(StatusItem, { label: "\u5DE5\u5177\u603B\u5F00\u5173", value: data.tools }),
|
|
77
78
|
React.createElement(StatusItem, { label: "\u5DE5\u5177\u63D0\u793A", value: data.toolPromptSwitch }),
|
|
78
79
|
React.createElement(StatusItem, { label: "\u5DE5\u5177\u63D0\u793A\u64A4\u56DE", value: data.toolPromptRevokeSwitch }),
|
|
79
|
-
React.createElement(StatusItem, { label: "\u5DE5\u5177\u63D0\u793A\u4F20\u53C2", value: data.toolPromptArgsSwitch })
|
|
80
|
+
React.createElement(StatusItem, { label: "\u5DE5\u5177\u63D0\u793A\u4F20\u53C2", value: data.toolPromptArgsSwitch }),
|
|
81
|
+
React.createElement(StatusItem, { label: "\u5DE5\u5177\u8C03\u7528\u5185\u5BB9", value: data.toolCallContentSwitch })),
|
|
80
82
|
React.createElement("div", { className: "pt-2 border-t border-white/10" },
|
|
81
83
|
React.createElement("div", { className: "flex items-center justify-between py-2" },
|
|
82
84
|
React.createElement("span", { className: " text-white/60" }, "\u5F53\u524D\u4F7F\u7528\u6A21\u578B"),
|
package/lib/middleware/mw.js
CHANGED
|
@@ -177,6 +177,7 @@ var mw = onMiddleware(selects, async (event, next) => {
|
|
|
177
177
|
// 处理回复消息
|
|
178
178
|
const replyData = event.value.message.find((item) => item.type === "reply");
|
|
179
179
|
if (replyData) {
|
|
180
|
+
event["replyMessageId"] = String(replyData.data.id);
|
|
180
181
|
const msg = await onebotClient.getMsg({
|
|
181
182
|
message_id: Number(replyData.data.id),
|
|
182
183
|
});
|
|
@@ -233,6 +234,7 @@ var mw = onMiddleware(selects, async (event, next) => {
|
|
|
233
234
|
})
|
|
234
235
|
.join("");
|
|
235
236
|
if (event.replyId && event.replyId !== "-1") {
|
|
237
|
+
event["replyMessageId"] = String(event.replyId);
|
|
236
238
|
const replyMsg = await client.getMessage(event.replyId);
|
|
237
239
|
console.log("获取回复", replyMsg);
|
|
238
240
|
if (replyMsg) {
|
|
@@ -276,6 +278,7 @@ var mw = onMiddleware(selects, async (event, next) => {
|
|
|
276
278
|
? [CDN_URL + event.value.fileMeta.url]
|
|
277
279
|
: [];
|
|
278
280
|
if (event.value.replyToId) {
|
|
281
|
+
event["replyMessageId"] = String(event.value.replyToId);
|
|
279
282
|
const channelMessages = await client.request({
|
|
280
283
|
method: "GET",
|
|
281
284
|
url: `/channels/${event.ChannelId}/messages/${event.value.replyToId}`,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import redisClient from '../../config.js';
|
|
2
2
|
import { loadSkills } from '../../api/loadSkill.js';
|
|
3
3
|
import App from '../../image/conponent/AiConfig.js';
|
|
4
|
-
import { useMessage,
|
|
4
|
+
import { useMessage, Text, Image } from 'alemonjs';
|
|
5
5
|
import { renderComponentIsHtmlToBuffer } from 'jsxp';
|
|
6
6
|
import OpenAi from 'openai';
|
|
7
7
|
|
|
@@ -54,10 +54,64 @@ const archiveCurrentConversation = async (guid, reason, aiConfig) => {
|
|
|
54
54
|
reason,
|
|
55
55
|
});
|
|
56
56
|
};
|
|
57
|
+
const statusNumberFormatter = new Intl.NumberFormat("zh-CN");
|
|
58
|
+
const formatNumberValue = (value) => {
|
|
59
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
60
|
+
? statusNumberFormatter.format(value)
|
|
61
|
+
: "未知";
|
|
62
|
+
};
|
|
63
|
+
const formatTokenValue = (value) => {
|
|
64
|
+
return formatNumberValue(value);
|
|
65
|
+
};
|
|
66
|
+
const formatAIContextStatus = (stats) => {
|
|
67
|
+
if (!stats) {
|
|
68
|
+
return "当前会话还没有 CAPI 上下文统计。";
|
|
69
|
+
}
|
|
70
|
+
const updatedAt = new Date(stats.updatedAt).toLocaleString("zh-CN", {
|
|
71
|
+
hour12: false,
|
|
72
|
+
});
|
|
73
|
+
const usageNote = stats.usageAvailable
|
|
74
|
+
? ""
|
|
75
|
+
: "提示: 最近一次请求未返回 usage, 实际 token 统计可能不完整。";
|
|
76
|
+
const divider = "················";
|
|
77
|
+
return [
|
|
78
|
+
"当前AI状态:",
|
|
79
|
+
divider,
|
|
80
|
+
`模式: ${stats.mode.toUpperCase()}`,
|
|
81
|
+
`模型: ${stats.model || "未知"}`,
|
|
82
|
+
`更新时间: ${updatedAt}`,
|
|
83
|
+
`请求次数: ${formatNumberValue(stats.requestCount)}`,
|
|
84
|
+
divider,
|
|
85
|
+
"最近一次上下文:",
|
|
86
|
+
`消息数: ${formatNumberValue(stats.contextMessageCount)}`,
|
|
87
|
+
`上下文字符数: ${formatNumberValue(stats.contextCharLength)}`,
|
|
88
|
+
`上下文估算token: ${formatTokenValue(stats.estimatedContextTokens)}`,
|
|
89
|
+
`输入token: ${formatTokenValue(stats.promptTokens)}`,
|
|
90
|
+
`输出token: ${formatTokenValue(stats.completionTokens)}`,
|
|
91
|
+
`总token: ${formatTokenValue(stats.totalTokens)}`,
|
|
92
|
+
`缓存命中token: ${formatTokenValue(stats.cachedTokens)}`,
|
|
93
|
+
`缓存未命中token: ${formatTokenValue(stats.cacheMissTokens)}`,
|
|
94
|
+
divider,
|
|
95
|
+
"当前会话累计:",
|
|
96
|
+
`输入token: ${formatTokenValue(stats.totalPromptTokens)}`,
|
|
97
|
+
`输出token: ${formatTokenValue(stats.totalCompletionTokens)}`,
|
|
98
|
+
`总token: ${formatTokenValue(stats.totalTokensUsed)}`,
|
|
99
|
+
`缓存命中token: ${formatTokenValue(stats.totalCachedTokens)}`,
|
|
100
|
+
`缓存未命中token: ${formatTokenValue(stats.totalCacheMissTokens)}`,
|
|
101
|
+
usageNote,
|
|
102
|
+
]
|
|
103
|
+
.filter((line) => line !== "")
|
|
104
|
+
.join("\n");
|
|
105
|
+
};
|
|
57
106
|
var res = onResponse(selects, async (e, next) => {
|
|
58
107
|
// 创建
|
|
59
108
|
const [message] = useMessage(e);
|
|
60
109
|
const config = await redisClient.getAIConfig(e.guid);
|
|
110
|
+
// 查看AI状态
|
|
111
|
+
if (/^(\/|#)ai状态$/i.test(e.msg)) {
|
|
112
|
+
const stats = await redisClient.getAIContextStats(e.guid);
|
|
113
|
+
message.send(format(Text(formatAIContextStatus(stats))));
|
|
114
|
+
}
|
|
61
115
|
// 查看AI配置
|
|
62
116
|
if (/^(\/|#)(ai配置|当前提示词|查看提示词)$/i.test(e.msg)) {
|
|
63
117
|
const complexResponse = (await redisClient.getComplexOutput(e.guid)) || "1"; // 复杂输出
|
|
@@ -73,6 +127,7 @@ var res = onResponse(selects, async (e, next) => {
|
|
|
73
127
|
const toolPromptSwitch = await redisClient.getToolPromptSwitch(e.guid); // 工具提示开关
|
|
74
128
|
const toolPromptRevokeSwitch = await redisClient.getToolPromptRevokeSwitch(e.guid); // 工具提示撤回开关
|
|
75
129
|
const toolPromptArgsSwitch = await redisClient.getToolPromptArgsSwitch(e.guid); // 工具提示传参开关
|
|
130
|
+
const toolCallContentSwitch = await redisClient.getToolCallContentSwitch(e.guid); // 工具调用内容发送开关
|
|
76
131
|
// 发送消息
|
|
77
132
|
try {
|
|
78
133
|
const scale = await redisClient.getRenderPrecision(e.guid);
|
|
@@ -93,6 +148,7 @@ var res = onResponse(selects, async (e, next) => {
|
|
|
93
148
|
toolPromptSwitch: toolPromptSwitch == "1",
|
|
94
149
|
toolPromptRevokeSwitch: toolPromptRevokeSwitch == "1",
|
|
95
150
|
toolPromptArgsSwitch: toolPromptArgsSwitch == "1",
|
|
151
|
+
toolCallContentSwitch: toolCallContentSwitch == "1",
|
|
96
152
|
}, {
|
|
97
153
|
playwright: {
|
|
98
154
|
context: { deviceScaleFactor: scale },
|
|
@@ -102,11 +158,11 @@ var res = onResponse(selects, async (e, next) => {
|
|
|
102
158
|
message.send(format(Image(img)));
|
|
103
159
|
}
|
|
104
160
|
else {
|
|
105
|
-
message.send(format(Text(`当前AI配置:\n`), Text(`总开关: ${config.model != "" ? "开启" : "关闭"}\n`), Text(`工具: ${toolsIsOpen == "1" ? "开启" : "关闭"}\n`), Text(`复杂输出: ${complexResponse == "1" ? "开启" : "关闭"}\n`), Text(`好感度开关: ${affectionIsOpen == "1" ? "开启" : "关闭"}\n`), Text(`TTS回复开关: ${isOpenTTSReply == "1" ? "开启" : "关闭"}\n`), Text(`深度思考: ${deepThinkingIsOpen == "1" ? "默认" : "关闭"}\n`), Text(`仅艾特触发: ${atTriggerSwitch == "1" ? "开启" : "关闭"}\n`), Text(`模型: ${config.model || "未设置"}\n`), Text(`提示词: ${config.systemPrompt || "未设置"}\n`), Text(`当前AI: ${currentAI || "未设置"}\n`), Text(`AI数量: ${aiList.length ? aiList.length : "暂无AI配置"}`)));
|
|
161
|
+
message.send(format(Text(`当前AI配置:\n`), Text(`总开关: ${config.model != "" ? "开启" : "关闭"}\n`), Text(`工具: ${toolsIsOpen == "1" ? "开启" : "关闭"}\n`), Text(`工具调用内容: ${toolCallContentSwitch == "1" ? "开启" : "关闭"}\n`), Text(`复杂输出: ${complexResponse == "1" ? "开启" : "关闭"}\n`), Text(`好感度开关: ${affectionIsOpen == "1" ? "开启" : "关闭"}\n`), Text(`TTS回复开关: ${isOpenTTSReply == "1" ? "开启" : "关闭"}\n`), Text(`深度思考: ${deepThinkingIsOpen == "1" ? "默认" : "关闭"}\n`), Text(`仅艾特触发: ${atTriggerSwitch == "1" ? "开启" : "关闭"}\n`), Text(`模型: ${config.model || "未设置"}\n`), Text(`提示词: ${config.systemPrompt || "未设置"}\n`), Text(`当前AI: ${currentAI || "未设置"}\n`), Text(`AI数量: ${aiList.length ? aiList.length : "暂无AI配置"}`)));
|
|
106
162
|
}
|
|
107
163
|
}
|
|
108
164
|
catch (error) {
|
|
109
|
-
message.send(format(Text(`当前AI配置:\n`), Text(`总开关: ${config.model != "" ? "开启" : "关闭"}\n`), Text(`工具: ${toolsIsOpen == "1" ? "开启" : "关闭"}\n`), Text(`复杂输出: ${complexResponse == "1" ? "开启" : "关闭"}\n`), Text(`好感度开关: ${affectionIsOpen == "1" ? "开启" : "关闭"}\n`), Text(`TTS回复开关: ${isOpenTTSReply == "1" ? "开启" : "关闭"}\n`), Text(`深度思考: ${deepThinkingIsOpen == "1" ? "默认" : "关闭"}\n`), Text(`仅艾特触发: ${atTriggerSwitch == "1" ? "开启" : "关闭"}\n`), Text(`模型: ${config.model || "未设置"}\n`), Text(`提示词: ${config.systemPrompt || "未设置"}\n`), Text(`当前AI: ${currentAI || "未设置"}\n`), Text(`AI数量: ${aiList.length ? aiList.length : "暂无AI配置"}`)));
|
|
165
|
+
message.send(format(Text(`当前AI配置:\n`), Text(`总开关: ${config.model != "" ? "开启" : "关闭"}\n`), Text(`工具: ${toolsIsOpen == "1" ? "开启" : "关闭"}\n`), Text(`工具调用内容: ${toolCallContentSwitch == "1" ? "开启" : "关闭"}\n`), Text(`复杂输出: ${complexResponse == "1" ? "开启" : "关闭"}\n`), Text(`好感度开关: ${affectionIsOpen == "1" ? "开启" : "关闭"}\n`), Text(`TTS回复开关: ${isOpenTTSReply == "1" ? "开启" : "关闭"}\n`), Text(`深度思考: ${deepThinkingIsOpen == "1" ? "默认" : "关闭"}\n`), Text(`仅艾特触发: ${atTriggerSwitch == "1" ? "开启" : "关闭"}\n`), Text(`模型: ${config.model || "未设置"}\n`), Text(`提示词: ${config.systemPrompt || "未设置"}\n`), Text(`当前AI: ${currentAI || "未设置"}\n`), Text(`AI数量: ${aiList.length ? aiList.length : "暂无AI配置"}`)));
|
|
110
166
|
}
|
|
111
167
|
}
|
|
112
168
|
// 查看AI列表
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useMessage, Text } from 'alemonjs';
|
|
2
|
+
|
|
3
|
+
const selects = onSelects(["message.create", "private.message.create"]);
|
|
4
|
+
const normalizeMessageId = (value) => {
|
|
5
|
+
if (value === undefined || value === null)
|
|
6
|
+
return undefined;
|
|
7
|
+
const text = String(value).trim();
|
|
8
|
+
return text === "" || text === "-1" ? undefined : text;
|
|
9
|
+
};
|
|
10
|
+
const getReplyMessageId = (e) => {
|
|
11
|
+
const replySegment = Array.isArray(e.value?.message)
|
|
12
|
+
? e.value.message.find((item) => item?.type === "reply")
|
|
13
|
+
: undefined;
|
|
14
|
+
return (normalizeMessageId(e.replyMessageId) ||
|
|
15
|
+
normalizeMessageId(replySegment?.data?.id) ||
|
|
16
|
+
normalizeMessageId(e.replyId) ||
|
|
17
|
+
normalizeMessageId(e.value?.replyToId) ||
|
|
18
|
+
normalizeMessageId(e.reply?.message_id) ||
|
|
19
|
+
normalizeMessageId(e.reply?.id));
|
|
20
|
+
};
|
|
21
|
+
var res = onResponse(selects, async (e) => {
|
|
22
|
+
const [message] = useMessage(e);
|
|
23
|
+
const targetMessageId = getReplyMessageId(e);
|
|
24
|
+
if (!targetMessageId) {
|
|
25
|
+
await message.send(format(Text("请引用一条消息后再发送 /撤回")));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const result = await message.delete({ messageId: targetMessageId });
|
|
29
|
+
if (result.code !== 2000) {
|
|
30
|
+
await message.send(format(Text("撤回失败,可能是平台不支持或机器人没有权限")));
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export { res as default, selects };
|
|
@@ -493,6 +493,22 @@ var res = onResponse(selects, async (e, next) => {
|
|
|
493
493
|
message.send(format(Text(`已${enable ? "开启" : "关闭"}工具提示详情功能 !`)));
|
|
494
494
|
return;
|
|
495
495
|
}
|
|
496
|
+
// 控制工具调用前 content 发送开关
|
|
497
|
+
if (/(\/|#)(开启|关闭)工具调用内容$/i.test(e.msg)) {
|
|
498
|
+
if (!e.IsMaster) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const match = e.msg.match(/(\/|#)(开启|关闭)工具调用内容$/i);
|
|
502
|
+
if (!match) {
|
|
503
|
+
message.send(format(Text("格式错误,请按照 格式:/开启工具调用内容 或 /关闭工具调用内容 进行设置")));
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const [, , action] = match;
|
|
507
|
+
const enable = action === "开启";
|
|
508
|
+
await redisClient.setToolCallContentSwitch(e.guid, enable);
|
|
509
|
+
message.send(format(Text(`已${enable ? "开启" : "关闭"}工具调用内容发送功能 !`)));
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
496
512
|
// 控制工具开关
|
|
497
513
|
if (/(\/|#)(开启|关闭)工具(.*)$/i.test(e.msg)) {
|
|
498
514
|
if (!e.IsMaster) {
|
|
@@ -43,6 +43,56 @@ const appendPendingGuidanceMessages = async (guid, messages) => {
|
|
|
43
43
|
await redisClient.addAIChatHistoryBatch(guid, guidanceMessages);
|
|
44
44
|
return guidanceMessages;
|
|
45
45
|
};
|
|
46
|
+
const getTextLength = (value) => {
|
|
47
|
+
if (typeof value === "string") {
|
|
48
|
+
return value.length;
|
|
49
|
+
}
|
|
50
|
+
if (Array.isArray(value)) {
|
|
51
|
+
return value.reduce((sum, item) => sum + getTextLength(item), 0);
|
|
52
|
+
}
|
|
53
|
+
if (value && typeof value === "object") {
|
|
54
|
+
return Object.values(value).reduce((sum, item) => sum + getTextLength(item), 0);
|
|
55
|
+
}
|
|
56
|
+
return 0;
|
|
57
|
+
};
|
|
58
|
+
const getNumber = (value) => {
|
|
59
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
60
|
+
};
|
|
61
|
+
const extractCachedTokens = (usage) => {
|
|
62
|
+
return getNumber(usage?.prompt_tokens_details?.cached_tokens ??
|
|
63
|
+
usage?.input_tokens_details?.cached_tokens ??
|
|
64
|
+
usage?.prompt_cache_hit_tokens);
|
|
65
|
+
};
|
|
66
|
+
const extractCacheMissTokens = (usage, promptTokens) => {
|
|
67
|
+
const explicitMissTokens = getNumber(usage?.prompt_cache_miss_tokens);
|
|
68
|
+
if (explicitMissTokens !== null) {
|
|
69
|
+
return explicitMissTokens;
|
|
70
|
+
}
|
|
71
|
+
const cachedTokens = extractCachedTokens(usage);
|
|
72
|
+
if (promptTokens !== null && cachedTokens !== null) {
|
|
73
|
+
return Math.max(promptTokens - cachedTokens, 0);
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
};
|
|
77
|
+
const recordCapiContextStats = async (guid, model, messages, completion) => {
|
|
78
|
+
const usage = completion?.usage;
|
|
79
|
+
const promptTokens = getNumber(usage?.prompt_tokens ?? usage?.input_tokens);
|
|
80
|
+
const completionTokens = getNumber(usage?.completion_tokens ?? usage?.output_tokens);
|
|
81
|
+
const contextCharLength = getTextLength(messages);
|
|
82
|
+
await redisClient.recordAIContextStats(guid, {
|
|
83
|
+
mode: "capi",
|
|
84
|
+
model,
|
|
85
|
+
contextMessageCount: messages.length,
|
|
86
|
+
contextCharLength,
|
|
87
|
+
estimatedContextTokens: Math.ceil(contextCharLength / 4),
|
|
88
|
+
promptTokens,
|
|
89
|
+
completionTokens,
|
|
90
|
+
totalTokens: getNumber(usage?.total_tokens),
|
|
91
|
+
cachedTokens: extractCachedTokens(usage),
|
|
92
|
+
cacheMissTokens: extractCacheMissTokens(usage, promptTokens),
|
|
93
|
+
usageAvailable: Boolean(usage),
|
|
94
|
+
});
|
|
95
|
+
};
|
|
46
96
|
const CApiReply = async (e) => {
|
|
47
97
|
console.log("e.UserId", e.UserId, e.bot);
|
|
48
98
|
const cfg = await getChatConfig(e);
|
|
@@ -81,6 +131,7 @@ const CApiReply = async (e) => {
|
|
|
81
131
|
createParams["stream"] = false;
|
|
82
132
|
// log("请求AI,参数:", createParams);
|
|
83
133
|
const stream = (await openai.chat.completions.create(createParams));
|
|
134
|
+
await recordCapiContextStats(e.guid, cfg.config.model, messages, stream);
|
|
84
135
|
log("AI回复原始数据:\n", JSON.stringify(stream, null, 2));
|
|
85
136
|
let fullContent = "";
|
|
86
137
|
let toolCalls = [];
|
|
@@ -114,7 +165,9 @@ const CApiReply = async (e) => {
|
|
|
114
165
|
return;
|
|
115
166
|
}
|
|
116
167
|
await redisClient.addAIChatHistory(e.guid, usermessage);
|
|
117
|
-
if (
|
|
168
|
+
if (cfg.toolConfig.toolCallContentSwitch === "1" &&
|
|
169
|
+
res.choices[0].message?.tool_calls &&
|
|
170
|
+
fullContent.trim() !== "") {
|
|
118
171
|
await sendAIReply(fullContent, cfg, e, message);
|
|
119
172
|
}
|
|
120
173
|
// 检查是否有工具调用需要处理
|
|
@@ -140,6 +193,7 @@ const CApiReply = async (e) => {
|
|
|
140
193
|
}
|
|
141
194
|
Object.assign(params, getDeepThoughtReasoning(cfg));
|
|
142
195
|
res = await openai.chat.completions.create(params);
|
|
196
|
+
await recordCapiContextStats(e.guid, cfg.config.model, messages, res);
|
|
143
197
|
if (!res.choices || res.choices.length === 0) {
|
|
144
198
|
log("AI未返回内容");
|
|
145
199
|
return;
|
|
@@ -243,6 +297,7 @@ const CApiReply = async (e) => {
|
|
|
243
297
|
// log("重新请求AI,参数:", params);
|
|
244
298
|
// 重新请求
|
|
245
299
|
res = await openai.chat.completions.create(params);
|
|
300
|
+
await recordCapiContextStats(e.guid, cfg.config.model, messages, res);
|
|
246
301
|
// 检查是否还有工具调用需要处理
|
|
247
302
|
if (!res.choices || res.choices.length === 0) {
|
|
248
303
|
log("AI未返回内容");
|
|
@@ -273,6 +328,7 @@ const CApiReply = async (e) => {
|
|
|
273
328
|
};
|
|
274
329
|
}
|
|
275
330
|
catch (error) {
|
|
331
|
+
await redisClient.setAIToolLoopProcessing(e.guid, false);
|
|
276
332
|
console.error("AI回复出错", error);
|
|
277
333
|
message.send(format(Text("AI回复出错了\n" + error)));
|
|
278
334
|
return {
|
|
@@ -75,6 +75,8 @@ const getChatConfig = async (e) => {
|
|
|
75
75
|
toolPromptRevokeSwitch: await redisClient.getToolPromptRevokeSwitch(e.guid),
|
|
76
76
|
/** 工具提示传参开关 */
|
|
77
77
|
toolPromptArgsSwitch: await redisClient.getToolPromptArgsSwitch(e.guid),
|
|
78
|
+
/** 工具调用前 content 发送开关 */
|
|
79
|
+
toolCallContentSwitch: await redisClient.getToolCallContentSwitch(e.guid),
|
|
78
80
|
};
|
|
79
81
|
/** 技能列表 */
|
|
80
82
|
const skills = loadSkills();
|
|
@@ -83,33 +85,34 @@ const getChatConfig = async (e) => {
|
|
|
83
85
|
/** 机器人昵称 */
|
|
84
86
|
const botName = e.bot.nickname || "小咸鱼";
|
|
85
87
|
const memoryPrompt = relevantMemoryContext
|
|
86
|
-
? `
|
|
87
|
-
## 系统预检索记忆
|
|
88
|
-
以下内容是系统根据当前话题自动检索到的相关长期记忆和历史归档, 回答时优先参考与当前话题直接相关的部分; 如果与用户本轮最新表述冲突, 以用户本轮消息为准
|
|
89
|
-
${relevantMemoryContext}
|
|
88
|
+
? `
|
|
89
|
+
## 系统预检索记忆
|
|
90
|
+
以下内容是系统根据当前话题自动检索到的相关长期记忆和历史归档, 回答时优先参考与当前话题直接相关的部分; 如果与用户本轮最新表述冲突, 以用户本轮消息为准
|
|
91
|
+
${relevantMemoryContext}
|
|
90
92
|
`
|
|
91
93
|
: "";
|
|
92
94
|
/** 系统提示词 */
|
|
93
|
-
const systemPrompt = `
|
|
94
|
-
# AI助手回复规范
|
|
95
|
-
请严格按照以下规范进行回复, 不要添加任何不必要的内容, 也不要删除或修改规范中的任何内容, 以确保回复能够被正确解析和处理
|
|
96
|
-
|
|
97
|
-
## 技能
|
|
98
|
-
在遇到用户需要执行特定操作时,先获取对应的技能,如果有可用技能,获取该技能,并严格按照技能文档要求的格式调用, 不要添加任何多余的内容, 也不要删除或修改规范中的任何内容, 以确保技能能够被正确解析和处理
|
|
99
|
-
当没有技能可以应对需求时, 就自行挑选合适的工具函数来完成用户的需求
|
|
100
|
-
如果用户提到本地仓库、知识库、参考项目、之前克隆过的项目,或者你准备说"本地没有"、"仓库里没有"、"没找到"之类的话,必须先检查 public/{guid} 内现有内容,优先搜索 knowledge/ 目录;至少先调用一次 AgentSearchFiles、AgentListFiles 或读取相关技能后再下结论
|
|
101
|
-
如果你不知道之前项目所在的 guid,先调用 AgentListWorkspaces 找到 public/ 下已有工作目录,再继续用 AgentListFiles、AgentSearchFiles 或 AgentReadFileLines;不要用 exec 查 public/{guid},因为 exec 的默认工作目录不是那里
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
95
|
+
const systemPrompt = `
|
|
96
|
+
# AI助手回复规范
|
|
97
|
+
请严格按照以下规范进行回复, 不要添加任何不必要的内容, 也不要删除或修改规范中的任何内容, 以确保回复能够被正确解析和处理
|
|
98
|
+
|
|
99
|
+
## 技能
|
|
100
|
+
在遇到用户需要执行特定操作时,先获取对应的技能,如果有可用技能,获取该技能,并严格按照技能文档要求的格式调用, 不要添加任何多余的内容, 也不要删除或修改规范中的任何内容, 以确保技能能够被正确解析和处理
|
|
101
|
+
当没有技能可以应对需求时, 就自行挑选合适的工具函数来完成用户的需求
|
|
102
|
+
如果用户提到本地仓库、知识库、参考项目、之前克隆过的项目,或者你准备说"本地没有"、"仓库里没有"、"没找到"之类的话,必须先检查 public/{guid} 内现有内容,优先搜索 knowledge/ 目录;至少先调用一次 AgentSearchFiles、AgentListFiles 或读取相关技能后再下结论
|
|
103
|
+
如果你不知道之前项目所在的 guid,先调用 AgentListWorkspaces 找到 public/ 下已有工作目录,再继续用 AgentListFiles、AgentSearchFiles 或 AgentReadFileLines;不要用 exec 查 public/{guid},因为 exec 的默认工作目录不是那里
|
|
104
|
+
如果用户需要直接执行服务器运维指令、查看服务状态、管理 Docker/PM2/systemctl、查看日志或部署服务,使用 RunServerCommand;它会自动审核指令安全,不要改用 exec。调用 RunServerCommand 时 reviewGuid 必填,必须传下方环境信息里的“当前群号(reviewGuid)”,不要传 public/{guid} 工作目录ID
|
|
105
|
+
如果用户要查看分支、确认当前分支、查看远端或切换分支,优先使用 AgentGitOperation,不要自己拼 git commit、push、pull、merge、rebase、reset 等命令
|
|
106
|
+
如果用户在继续之前的话题、问你记不记得、提到上次/刚才/之前聊过的内容,先参考“系统预检索记忆”;如果系统预检索内容还不够,再调用 MemoryOperation,优先使用 chatHistory 的 search:关键词 或 guid:id 去补充历史上下文,然后再回答
|
|
107
|
+
|
|
108
|
+
调用执行脚本类型的工具时, 务必确保脚本安全, 不得执行对服务器有伤害的脚本
|
|
109
|
+
---
|
|
110
|
+
技能列表:
|
|
111
|
+
${skills.map((skill) => `- ${skill.name}: ${skill.description}`).join("\n")}
|
|
112
|
+
---
|
|
113
|
+
${memoryPrompt}
|
|
114
|
+
|
|
115
|
+
## 关于回复格式:
|
|
113
116
|
### 用户发言
|
|
114
117
|
- 格式: [私聊]用户昵称(用户id)(发送时间):消息内容
|
|
115
118
|
当场景为私聊时, 用户昵称前方会出现[私聊]标识
|
|
@@ -158,15 +161,15 @@ const getChatConfig = async (e) => {
|
|
|
158
161
|
#### 基础状态信息:
|
|
159
162
|
这里的信息会实时变化,根据需要进行获取使用
|
|
160
163
|
当前群时间:${getGroupTimeString()}
|
|
161
|
-
|
|
164
|
+
当前群号(reviewGuid):${e.guid}
|
|
162
165
|
当前群名称:${e.GroupName || "无"}
|
|
163
166
|
当前聊天平台:${e.Platform}
|
|
164
167
|
当前框架:alemonjs-aichat
|
|
165
168
|
${e.ClientError ? `当前错误信息:${e.ClientError}` : ""}
|
|
166
169
|
`;
|
|
167
|
-
const RapiSystemPrompt = `
|
|
168
|
-
请严格按照以下规范进行回复, 以确保回复能够被正确解析和处理
|
|
169
|
-
1. 文本消息:
|
|
170
|
+
const RapiSystemPrompt = `
|
|
171
|
+
请严格按照以下规范进行回复, 以确保回复能够被正确解析和处理
|
|
172
|
+
1. 文本消息:
|
|
170
173
|
${botName}::text<<<EOF
|
|
171
174
|
文本内容
|
|
172
175
|
EOF
|
|
@@ -185,14 +188,14 @@ ${botName}::audio voice=音色 内容
|
|
|
185
188
|
=====================
|
|
186
189
|
|
|
187
190
|
- 不允许使用任何其他格式
|
|
188
|
-
- 不允许输出解释说明
|
|
189
|
-
- 不允许夹杂 markdown
|
|
190
|
-
- tool调用前可以先输出一行说明
|
|
191
|
-
- 每一条输出必须独立一行
|
|
192
|
-
- 允许多段输出叠加, 例如:
|
|
193
|
-
${botName}::text 这是第一条消息
|
|
194
|
-
${botName}::text 这是第二条消息
|
|
195
|
-
${memoryPrompt ? `\n【系统预检索记忆】\n${relevantMemoryContext}\n` : ""}
|
|
191
|
+
- 不允许输出解释说明
|
|
192
|
+
- 不允许夹杂 markdown
|
|
193
|
+
- tool调用前可以先输出一行说明
|
|
194
|
+
- 每一条输出必须独立一行
|
|
195
|
+
- 允许多段输出叠加, 例如:
|
|
196
|
+
${botName}::text 这是第一条消息
|
|
197
|
+
${botName}::text 这是第二条消息
|
|
198
|
+
${memoryPrompt ? `\n【系统预检索记忆】\n${relevantMemoryContext}\n` : ""}
|
|
196
199
|
`;
|
|
197
200
|
return {
|
|
198
201
|
/** AI配置 */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { format, Audio,
|
|
1
|
+
import { format, Audio, ImageURL, ImageFile, Text, Mention } from 'alemonjs';
|
|
2
2
|
import redisClient from '../../config.js';
|
|
3
3
|
import { TTSClient } from '../../api/tts.js';
|
|
4
4
|
import { createTTSMessage } from './tts.js';
|
package/lib/routes/commands.js
CHANGED
|
@@ -7,6 +7,11 @@ const children = [
|
|
|
7
7
|
regular: /(\/|#)(ai)?(指令|帮助|菜单|功能)$/i,
|
|
8
8
|
handler: lazy(() => import('../response/help/res.js')),
|
|
9
9
|
},
|
|
10
|
+
// revoke
|
|
11
|
+
{
|
|
12
|
+
regular: /^(\/|#)撤回$/i,
|
|
13
|
+
handler: lazy(() => import('../response/revoke/res.js')),
|
|
14
|
+
},
|
|
10
15
|
// tools (多条正则合并)
|
|
11
16
|
{
|
|
12
17
|
regular: Regular.or(/(\/|#)(ai)?(pt|p图|ps|修图)(.*)$/i, /(\/|#)(ai)?(画图)(.*)$/i, /(\/|#)(ai)?(生成视频)(.*)$/i, /(\/|#)(ai)?(查询)(.*)$/i, /(\/|#)(ai)?(开启|关闭)?画图提示词优化$/i, /^(\/|#)(.+)说([\s\S]*)$/i, /^(\/|#)安装语音(模型)?(.+)$/i, /^(\/|#)看魔法$/i),
|
|
@@ -14,12 +19,12 @@ const children = [
|
|
|
14
19
|
},
|
|
15
20
|
// config
|
|
16
21
|
{
|
|
17
|
-
regular: Regular.or(/^(\/|#)(ai配置|当前提示词|查看提示词)$/i, /^(\/|#)ai列表$/i, /^(\/|#)提示词列表$/i, /^(\/|#)清空对话$/i, /^(\/|#)清空(所有|全部)对话$/i, /^(\/|#)get (.+)$/i, /(\/|#)(开启)?(new|新对话|新会话)$/i),
|
|
22
|
+
regular: Regular.or(/^(\/|#)(ai配置|当前提示词|查看提示词)$/i, /^(\/|#)ai列表$/i, /^(\/|#)提示词列表$/i, /^(\/|#)清空对话$/i, /^(\/|#)清空(所有|全部)对话$/i, /^(\/|#)get (.+)$/i, /(\/|#)(开启)?(new|新对话|新会话)$/i, /^(\/|#)ai状态$/),
|
|
18
23
|
handler: lazy(() => import('../response/config/res.js')),
|
|
19
24
|
},
|
|
20
25
|
// setting (包含大量子命令)
|
|
21
26
|
{
|
|
22
|
-
regular: Regular.or(/(\/|#)添加ai$/i, /(\/|#)添加ai\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/i, /(\/|#)(切换)(ai|配置) ?(.*)/i, /(\/|#)添加提示词$/i, /(\/|#)添加提示词 (\S+) ([\s\S]+)/i, /(\/|#)(删除)(提示词|提示词列表) ?(.*)/i, /(\/|#)(切换|设置)提示词 ?(.*)$/i, /(\/|#)清空提示词$/i, /(\/|#)删除ai\s(\S+)$/i, /(\/|#)(开启|启用|关闭|禁用)好感度$/i, /(\/|#)(开启|关闭)(全部)?聊天$/i, /(\/|#)(开启|关闭)语音回复$/i, /(\/|#)切换图床(.*)$/i, /(\/|#)修改 ?(.+) ?模型 ?(.+)$/i, /(\/|#)(关闭|开启)复杂(输出|回复)$/i, /(\/|#)(关闭|禁止|不可以|不允许|开启|可以|允许)(涩涩|色色)$/i, /(\/|#)切换模型(.*)$/i, /(\/|#)(关闭|开启)主动(搭话|回复)$/i, /(\/|#)(开启|关闭)深度思考$/i, /(\/|#)(设置|修改)(ai)?(.*)rapi模式(开启|关闭)$/i, /(\/|#)(开启|关闭)仅艾特触发$/i, /(\/|#)添加正则规则 (.+)$/i, /(\/|#)(删除|移除)正则规则 (.+)$/i, /(\/|#)正则规则列表$/i, /(\/|#)清空正则规则$/i, /(\/|#)切换群容器$/i, /(\/|#)切换用户容器$/i, /(\/|#)切换系统环境$/i, /(\/|#)工具列表$/i, /(\/|#)(开启|关闭)(MCP)?工具$/i, /(\/|#)(开启|关闭)工具(.*)$/i, /(\/|#)(开启|关闭)工具提示$/i, /(\/|#)(开启|关闭)工具(调用)?提示撤回$/i, /(\/|#)(开启|关闭)工具(调用)?提示详情$/i, /(\/|#)测试$/i, /(\/|#)设置渲染精度\s*(中|高|超高)$/i),
|
|
27
|
+
regular: Regular.or(/(\/|#)添加ai$/i, /(\/|#)添加ai\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/i, /(\/|#)(切换)(ai|配置) ?(.*)/i, /(\/|#)添加提示词$/i, /(\/|#)添加提示词 (\S+) ([\s\S]+)/i, /(\/|#)(删除)(提示词|提示词列表) ?(.*)/i, /(\/|#)(切换|设置)提示词 ?(.*)$/i, /(\/|#)清空提示词$/i, /(\/|#)删除ai\s(\S+)$/i, /(\/|#)(开启|启用|关闭|禁用)好感度$/i, /(\/|#)(开启|关闭)(全部)?聊天$/i, /(\/|#)(开启|关闭)语音回复$/i, /(\/|#)切换图床(.*)$/i, /(\/|#)修改 ?(.+) ?模型 ?(.+)$/i, /(\/|#)(关闭|开启)复杂(输出|回复)$/i, /(\/|#)(关闭|禁止|不可以|不允许|开启|可以|允许)(涩涩|色色)$/i, /(\/|#)切换模型(.*)$/i, /(\/|#)(关闭|开启)主动(搭话|回复)$/i, /(\/|#)(开启|关闭)深度思考$/i, /(\/|#)(设置|修改)(ai)?(.*)rapi模式(开启|关闭)$/i, /(\/|#)(开启|关闭)仅艾特触发$/i, /(\/|#)添加正则规则 (.+)$/i, /(\/|#)(删除|移除)正则规则 (.+)$/i, /(\/|#)正则规则列表$/i, /(\/|#)清空正则规则$/i, /(\/|#)切换群容器$/i, /(\/|#)切换用户容器$/i, /(\/|#)切换系统环境$/i, /(\/|#)工具列表$/i, /(\/|#)(开启|关闭)(MCP)?工具$/i, /(\/|#)(开启|关闭)工具(.*)$/i, /(\/|#)(开启|关闭)工具提示$/i, /(\/|#)(开启|关闭)工具(调用)?提示撤回$/i, /(\/|#)(开启|关闭)工具(调用)?提示详情$/i, /(\/|#)(开启|关闭)工具调用内容$/i, /(\/|#)测试$/i, /(\/|#)设置渲染精度\s*(中|高|超高)$/i),
|
|
23
28
|
handler: lazy(() => import('../response/setting/res.js')),
|
|
24
29
|
},
|
|
25
30
|
// affection
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alemonjs-aichat",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.38",
|
|
4
4
|
"description": "alemonjs-aichat",
|
|
5
5
|
"author": "suancaixianyu",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@ai-sdk/xai": "^3.0.60",
|
|
48
|
+
"@alemonjs/scbbs": "file:../scbbs",
|
|
48
49
|
"@aws-sdk/client-s3": "^3.975.0",
|
|
49
50
|
"@aws-sdk/s3-request-presigner": "^3.975.0",
|
|
50
51
|
"ai": "^6.0.105",
|