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.
@@ -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: ["currentBranch", "listBranches", "listRemotes", "switchBranch"],
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
- { pattern: /&&|\|\||[;|<>`]|[$][(]/, reason: "不允许使用 shell 控制符、重定向、命令替换或反引号" },
834
- { pattern: /(^|[\s])(rm|del|erase|rmdir|rd)([\s]|$)/i, reason: "不允许使用删除命令" },
835
- { pattern: /(^|[\s])(powershell|pwsh|cmd|bash|sh|docker|ssh|scp|sftp|curl|wget)([\s]|$)/i, reason: "不允许执行受限外部命令" },
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 = ["new", "restore", "build", "run", "test", "publish", "clean"].includes(subcommand)
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) || ["npx", "npx.cmd", "bunx"].includes(baseCommand)) {
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) || 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: "你是一个命令安全审查器。请只输出 JSON,格式为 {\"allowed\":boolean,\"risk\":\"low|medium|high|blocked\",\"reason\":\"简短中文原因\"}。审核标准:命令必须只适用于 public 工作目录内的安全项目操作,不得包含权限提升、删除系统文件、远程控制、数据外传、全局环境修改或危险 shell 行为。"
1552
- + `\n性格约束:${aiConfig.config.systemPrompt || ""}`,
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
- { name: "dotnet", file: process.platform === "win32" ? "dotnet.exe" : "dotnet", args: ["--version"] },
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
- { name: "node", file: process.platform === "win32" ? "node.exe" : "node", args: ["-v"] },
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 ? result.output || "ok" : `exit ${result.exitCode}`;
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 ? `unavailable: ${error.message}` : String(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.readdirSync(cwd, { withFileTypes: true }).slice(0, 50).map((item) => ({
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") || lower.endsWith(".tar.gz") || lower.endsWith(".tgz")) {
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", ["inspect", "-f", "{{.State.Running}}", containerName]);
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", "-d",
1791
- "--name", containerName,
1792
- "--memory", memory || "256m",
1793
- "--cpus", cpus || "0.5",
1794
- "--pids-limit", String(pidsLimit || 64),
1795
- "--network", networkMode || "none",
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) || !fs.statSync(archive.target).isFile()) {
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.redis.del(`ai:history:${guid}`);
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
- await this.redis.set(`ai:tool_loop_processing:${guid}`, "1");
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
- return (await this.redis.get(`ai:tool_loop_processing:${guid}`)) || "0";
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
- return JSON.parse(queueStr);
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
  /** 提取并清空工具调用期间的引导消息 */
@@ -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"),
@@ -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, Image, Text } from 'alemonjs';
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 (res.choices[0].message?.tool_calls && fullContent.trim() !== "") {
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
- 如果用户要查看分支、确认当前分支、查看远端或切换分支,优先使用 AgentGitOperation,不要自己拼 git commit、push、pull、merge、rebase、reset 等命令
103
- 如果用户在继续之前的话题、问你记不记得、提到上次/刚才/之前聊过的内容,先参考“系统预检索记忆”;如果系统预检索内容还不够,再调用 MemoryOperation,优先使用 chatHistory search:关键词 或 guid:id 去补充历史上下文,然后再回答
104
-
105
- 调用执行脚本类型的工具时, 务必确保脚本安全, 不得执行对服务器有伤害的脚本
106
- ---
107
- 技能列表:
108
- ${skills.map((skill) => `- ${skill.name}: ${skill.description}`).join("\n")}
109
- ---
110
- ${memoryPrompt}
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
- 当前群号:${e.guid}
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, Mention, Text, ImageURL, ImageFile } from 'alemonjs';
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';
@@ -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.35",
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",