Compare commits
3 Commits
3848dd82d9
...
674c3cbd44
| Author | SHA1 | Date | |
|---|---|---|---|
| 674c3cbd44 | |||
| c5b61ee3d8 | |||
| cd8d2683c0 |
@@ -28,13 +28,8 @@
|
||||
"@types/fluent-ffmpeg": "^2.1.28",
|
||||
"@types/node": "^25.6.1",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.2",
|
||||
"@typescript-eslint/parser": "^8.59.2",
|
||||
"@typescript/native-preview": "^7.0.0-beta",
|
||||
"drizzle-kit": "^0.31.10",
|
||||
"eslint": "^10.3.0",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.59.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -99,30 +94,8 @@
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
||||
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
||||
|
||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||
|
||||
"@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="],
|
||||
|
||||
"@eslint/config-helpers": ["@eslint/config-helpers@0.5.5", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w=="],
|
||||
|
||||
"@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="],
|
||||
|
||||
"@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="],
|
||||
|
||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.1", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ=="],
|
||||
|
||||
"@google/genai": ["@google/genai@2.0.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-6XpO+YbGutXkm5QgR7NZktISxSz0dw3pSs9NtCUQwvhJc1eyA3KhdKhE/0Uaxp3a6eul3LC0SKau1bXymjOKUg=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||
|
||||
"@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
|
||||
|
||||
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
|
||||
|
||||
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
||||
|
||||
"@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="],
|
||||
|
||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
|
||||
@@ -253,14 +226,8 @@
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="],
|
||||
|
||||
"@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/fluent-ffmpeg": ["@types/fluent-ffmpeg@2.1.28", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ovxsDwBcPfJ+eYs1I/ZpcYCnkce7pvH9AHSvrZllAp1ZPpTRDZAFjF3TRFbukxSgIYTTNYePbS0rKUmaxVbXw=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/node": ["@types/node@25.6.1", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-coJCN8O1q4AGyyqCAUSP06P+SrMTu18BkEj3NVAK07q6QUneD2wzj3CLv9+yP+BMeZQlMvneXqqvDe3w+xcq7g=="],
|
||||
|
||||
"@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="],
|
||||
@@ -269,34 +236,24 @@
|
||||
|
||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/type-utils": "8.59.2", "@typescript-eslint/utils": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ=="],
|
||||
"@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260421.2", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260421.2", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260421.2", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260421.2", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260421.2", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260421.2", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260421.2", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260421.2" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-CmajHI25HpVWE9R1XFoxr+cphJPxoYD3eFioQtAvXYkMFKnLdICMS9pXre9Pybizb75ejRxjKD5/CVG055rEIg=="],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ=="],
|
||||
"@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260421.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fHv1r3ZmVo6zxuAIFmuX3w9QxbcauoG0SsWhmDwm6VmRubLlOJIcmTtlmV3JAb9oOnq8LuzZljzT7Q39fSMQDw=="],
|
||||
|
||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.2", "@typescript-eslint/types": "^8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw=="],
|
||||
"@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260421.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-KWTR6xbW9t+JS7D5DQIzo75pqVXVWUxF9PMv/+S6xsnOjCVd6g0ixHcFpFMJMKSUQpGPr8Z5f7b8ks6LHW01jg=="],
|
||||
|
||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2" } }, "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg=="],
|
||||
"@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260421.2", "", { "os": "linux", "cpu": "arm" }, "sha512-BWLQO3nemLDSV5PoE5GPHe1dU9Dth77Kv8/cle9Ujcp4LhPo0KincdPqFH/qKeU/xvW25mgFueflZ1nc4rKuww=="],
|
||||
|
||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw=="],
|
||||
"@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260421.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-VLMEuml3BhUb+jaL0TXQ4xvVODxJF+RhkI+tBWvlynsJI4khTXEiwWh+wPOJrsfBRYFRMXEu28Odl/HXkYze8w=="],
|
||||
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/utils": "8.59.2", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ=="],
|
||||
"@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260421.2", "", { "os": "linux", "cpu": "x64" }, "sha512-qUrJWTB5/wv4wnRG0TRXElAxc2kykNiRNyEIEqBbLmzDlrcvAW7RRy8MXoY1ZyTiKGMu14itZ3x9oW6+blFpRw=="],
|
||||
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.59.2", "", {}, "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q=="],
|
||||
"@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260421.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Rc6NsWlZmCs5YUKVzKgwoBOoRUGsPzct4BDMRX0csD1devLBBc4AbUXWKsJRbpwIAnqMO1ld4sNHEb+wXgfNHQ=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.2", "@typescript-eslint/tsconfig-utils": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg=="],
|
||||
|
||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA=="],
|
||||
|
||||
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
"@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260421.2", "", { "os": "win32", "cpu": "x64" }, "sha512-GQv1+dya1t6EqF2Cpsb+xoozovdX10JUSf6Kl/8xNkTapzmlHd+uMr+8ku3jIASTxoRGn0Mklgjj3MDKrOTuLg=="],
|
||||
|
||||
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||
|
||||
"ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
@@ -307,13 +264,13 @@
|
||||
|
||||
"axios": ["axios@1.16.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w=="],
|
||||
|
||||
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
|
||||
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
||||
|
||||
@@ -341,8 +298,6 @@
|
||||
|
||||
"decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="],
|
||||
|
||||
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||
|
||||
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
@@ -373,43 +328,11 @@
|
||||
|
||||
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"eslint": ["eslint@10.3.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.5.5", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw=="],
|
||||
|
||||
"eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
|
||||
|
||||
"eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
||||
|
||||
"espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="],
|
||||
|
||||
"esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
|
||||
|
||||
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
||||
|
||||
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||
|
||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||
|
||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||
|
||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
|
||||
|
||||
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
||||
|
||||
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
||||
|
||||
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
||||
|
||||
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
|
||||
"find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
|
||||
|
||||
"fluent-ffmpeg": ["fluent-ffmpeg@2.1.3", "", { "dependencies": { "async": "^0.2.9", "which": "^1.1.1" } }, "sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q=="],
|
||||
|
||||
@@ -437,12 +360,10 @@
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="],
|
||||
"get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="],
|
||||
|
||||
"glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
|
||||
"google-auth-library": ["google-auth-library@10.5.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w=="],
|
||||
|
||||
"google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
|
||||
@@ -461,16 +382,8 @@
|
||||
|
||||
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||
|
||||
"ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||
|
||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
||||
@@ -479,25 +392,15 @@
|
||||
|
||||
"json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
|
||||
|
||||
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
||||
|
||||
"jsonfile": ["jsonfile@5.0.0", "", { "dependencies": { "universalify": "^0.1.2" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w=="],
|
||||
|
||||
"jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
|
||||
|
||||
"jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
|
||||
|
||||
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||
|
||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
|
||||
"libsql": ["libsql@0.5.29", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.29", "@libsql/darwin-x64": "0.5.29", "@libsql/linux-arm-gnueabihf": "0.5.29", "@libsql/linux-arm-musleabihf": "0.5.29", "@libsql/linux-arm64-gnu": "0.5.29", "@libsql/linux-arm64-musl": "0.5.29", "@libsql/linux-x64-gnu": "0.5.29", "@libsql/linux-x64-musl": "0.5.29", "@libsql/win32-x64-msvc": "0.5.29" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg=="],
|
||||
|
||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||
"locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
|
||||
|
||||
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
||||
|
||||
@@ -509,14 +412,12 @@
|
||||
|
||||
"mime-types": ["mime-types@2.1.29", "", { "dependencies": { "mime-db": "1.46.0" } }, "sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ=="],
|
||||
|
||||
"minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
|
||||
"minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||
|
||||
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
|
||||
|
||||
"node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
||||
@@ -525,11 +426,9 @@
|
||||
|
||||
"openai": ["openai@6.37.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-0H5dEGFmmLv6KSd0W1w2nyL8WsLkX6yoLeQpU+dZAOuGcany5qkYQMmj35ZrKgb6yiyYqpUzFOpR8mZQkgqeEQ=="],
|
||||
|
||||
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
||||
"p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
||||
|
||||
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||
|
||||
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||
"p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
|
||||
|
||||
"p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="],
|
||||
|
||||
@@ -543,20 +442,14 @@
|
||||
|
||||
"path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
|
||||
"pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="],
|
||||
|
||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||
|
||||
"promise-limit": ["promise-limit@2.7.0", "", {}, "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw=="],
|
||||
|
||||
"protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="],
|
||||
|
||||
"proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="],
|
||||
|
||||
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||
@@ -597,10 +490,6 @@
|
||||
|
||||
"systeminformation": ["systeminformation@5.31.6", "", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-Uv2b2uGGM6ns+26czgW2cYRabYdnswM0ddSOOlryHOaelzsmDSet1iM/NT7VOYxW8x/BW+HkY+b1Ve2pLTSGSA=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
"ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="],
|
||||
@@ -609,20 +498,12 @@
|
||||
|
||||
"twemoji-parser": ["twemoji-parser@14.0.0", "", {}, "sha512-9DUOTGLOWs0pFWnh1p6NF+C3CkQ96PWmEFwhOVmT3WbecRC+68AIqpsnJXygfkFcp4aXbOp8Dwbhh/HQgvoRxA=="],
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
|
||||
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
||||
|
||||
"typescript-eslint": ["typescript-eslint@8.59.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.2", "@typescript-eslint/parser": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/utils": "8.59.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ=="],
|
||||
|
||||
"typescript-telegram-bot-api": ["typescript-telegram-bot-api@0.16.0", "", { "dependencies": { "axios": "^1.7.7", "form-data": "^4.0.1" } }, "sha512-NaAjXucQiZ87U8La/IMaWDOghbMlJzfMbU4rG8ppFpgPOvEwat/zfN5BM+J2QDvKVGN87qJ+1nELnkm3ctSLnQ=="],
|
||||
|
||||
"undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
|
||||
|
||||
"universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
|
||||
|
||||
"whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="],
|
||||
@@ -631,8 +512,6 @@
|
||||
|
||||
"which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="],
|
||||
|
||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
|
||||
|
||||
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
@@ -645,18 +524,12 @@
|
||||
|
||||
"yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
|
||||
|
||||
"zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
|
||||
|
||||
"@esbuild-kit/esm-loader/get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="],
|
||||
|
||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||
|
||||
"@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
||||
@@ -673,18 +546,12 @@
|
||||
|
||||
"@types/ws/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
|
||||
"bun-types/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
|
||||
|
||||
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
|
||||
|
||||
"glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="],
|
||||
|
||||
"protobufjs/@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="],
|
||||
@@ -695,7 +562,7 @@
|
||||
|
||||
"tsx/esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="],
|
||||
|
||||
"yargs/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
|
||||
"tsx/get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
|
||||
|
||||
@@ -753,8 +620,6 @@
|
||||
|
||||
"@types/ws/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"protobufjs/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="],
|
||||
@@ -808,13 +673,5 @@
|
||||
"tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="],
|
||||
|
||||
"yargs/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
|
||||
|
||||
"glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"yargs/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
|
||||
|
||||
"yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+326
-1342
File diff suppressed because it is too large
Load Diff
+4
-11
@@ -2,20 +2,18 @@
|
||||
"name": "tg-chat-bot",
|
||||
"main": "src/index.ts",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"lint": "eslint .",
|
||||
"build": "tsgo -p tsconfig.json",
|
||||
"start": "node dist/index.js",
|
||||
"bun:start": "bun run dist/index.js"
|
||||
"bun:start": "bun run src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/genai": "^2.0.0",
|
||||
"@mistralai/mistralai": "^2.2.1",
|
||||
"openai": "^6.37.0",
|
||||
"ollama": "^0.6.3",
|
||||
|
||||
"typescript-telegram-bot-api": "^0.16.0",
|
||||
|
||||
"@libsql/client": "^0.17.3",
|
||||
"@napi-rs/canvas": "^1.0.0",
|
||||
"axios": "^1.16.0",
|
||||
@@ -34,12 +32,7 @@
|
||||
"@types/fluent-ffmpeg": "^2.1.28",
|
||||
"@types/node": "^25.6.1",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.2",
|
||||
"@typescript-eslint/parser": "^8.59.2",
|
||||
"drizzle-kit": "^0.31.10",
|
||||
"eslint": "^10.3.0",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.59.2"
|
||||
"@typescript/native-preview": "^7.0.0-beta"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "../logging/ai-logger";
|
||||
@@ -207,7 +207,7 @@ export function createMistralClient(target: AiRuntimeTarget): Mistral {
|
||||
|
||||
export function createOllamaClient(target: AiRuntimeTarget): Ollama {
|
||||
return new Ollama({
|
||||
host: target.baseUrl?.endsWith(":11434") ? target.baseUrl : target.baseUrl + ":11434",
|
||||
host: target.baseUrl,
|
||||
headers: target.apiKey ? {"Authorization": `Bearer ${target.apiKey}`} : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -65,26 +65,5 @@ export function asMistralChatMessage(message: ChatMessage): MistralChatMessage {
|
||||
// }
|
||||
// }
|
||||
|
||||
/*
|
||||
const messages: any[] = ordered.map(part => {
|
||||
const content: any[] = [{
|
||||
type: "input_text",
|
||||
text: (Environment.USE_NAMES_IN_PROMPT && !part.bot ? `MESSAGE FROM USER \"${part.name}\":\n` : "") + part.content,
|
||||
}];
|
||||
|
||||
if (!part.bot) {
|
||||
for (const image of part.images ?? []) {
|
||||
content.push({type: "input_image", image_url: `data:image/jpeg;base64,${image}`, detail: "auto"});
|
||||
}
|
||||
}
|
||||
|
||||
return {role: part.bot ? "assistant" : "user", content};
|
||||
});
|
||||
|
||||
if (Environment.SYSTEM_PROMPT && Environment.USE_SYSTEM_PROMPT) {
|
||||
messages.unshift({role: "system", content: Environment.SYSTEM_PROMPT});
|
||||
}
|
||||
return {parts: messages, imageCount};
|
||||
*/
|
||||
|
||||
export type AiChatMessage = | OpenAIChatMessage | OllamaChatMessage | MistralChatMessage | GeminiMessage;
|
||||
|
||||
@@ -67,7 +67,7 @@ export type MistralContentChunk =
|
||||
|
||||
export type MistralFunctionCall = {
|
||||
name: string;
|
||||
arguments: { [k: string]: any } | string;
|
||||
arguments: Record<string, unknown> | string;
|
||||
};
|
||||
|
||||
export type MistralToolCall = {
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {ResponseInputItem} from "openai/resources/responses/responses";
|
||||
import type {ResponseInputMessageContentList} from "openai/resources/responses/responses";
|
||||
|
||||
export type OpenAIChatMessage = ResponseInputItem
|
||||
export type OpenAIChatMessage = {
|
||||
type: "message";
|
||||
role: "system" | "user" | "assistant";
|
||||
content: string | ResponseInputMessageContentList;
|
||||
};
|
||||
|
||||
@@ -295,26 +295,38 @@ export async function formatRuntimeModelInfo(
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
type NamedModel = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
model?: string;
|
||||
};
|
||||
|
||||
type ModelListResponse = {
|
||||
models?: NamedModel[];
|
||||
data?: NamedModel[];
|
||||
};
|
||||
|
||||
export async function listProviderModels(provider: AiProvider): Promise<string[]> {
|
||||
const target = resolveAiRuntimeTarget(provider, "chat", getRuntimeModel(provider));
|
||||
|
||||
switch (provider) {
|
||||
case AiProvider.OLLAMA: {
|
||||
const ollama = createOllamaClient(target);
|
||||
const result: any = await ollama.list();
|
||||
return (result.models ?? []).map((m: any) => m.model || m.name).filter(Boolean);
|
||||
const result = await ollama.list() as ModelListResponse;
|
||||
return (result.models ?? []).map(m => m.model || m.name).filter((name): name is string => !!name);
|
||||
}
|
||||
case AiProvider.GEMINI: {
|
||||
const models: string[] = [];
|
||||
if (getGeminiApiMode(target) === "openai") {
|
||||
const geminiAi = createGeminiOpenAiClient(target);
|
||||
const iterable: any = await geminiAi.models.list();
|
||||
const iterable = await geminiAi.models.list() as AsyncIterable<NamedModel>;
|
||||
for await (const model of iterable) models.push(model.name || model.id || String(model));
|
||||
return models;
|
||||
}
|
||||
|
||||
const geminiAi = createGoogleGenAiClient(target);
|
||||
const iterable: any = await geminiAi.models.list();
|
||||
const iterable = await geminiAi.models.list() as AsyncIterable<NamedModel>;
|
||||
for await (const model of iterable) {
|
||||
const name = model.name || model.id || String(model);
|
||||
models.push(String(name).replace(/^models\//, ""));
|
||||
@@ -323,13 +335,14 @@ export async function listProviderModels(provider: AiProvider): Promise<string[]
|
||||
}
|
||||
case AiProvider.MISTRAL: {
|
||||
const mistralAi = createMistralClient(target);
|
||||
const result: any = await mistralAi.models.list();
|
||||
return (result.data ?? result.models ?? result ?? []).map((m: any) => m.id || m.name || String(m)).filter(Boolean);
|
||||
const result = await mistralAi.models.list() as ModelListResponse | NamedModel[];
|
||||
const items = Array.isArray(result) ? result : result.data ?? result.models ?? [];
|
||||
return items.map(m => m.id || m.name || String(m)).filter((name): name is string => !!name);
|
||||
}
|
||||
case AiProvider.OPENAI: {
|
||||
const openAi = createOpenAiClient(target);
|
||||
const result: any = await openAi.models.list();
|
||||
return (result.data ?? []).map((m: any) => m.id).filter(Boolean);
|
||||
const result = await openAi.models.list() as ModelListResponse;
|
||||
return (result.data ?? []).map(m => m.id).filter((id): id is string => !!id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import {Environment} from "../common/environment";
|
||||
import {AiProvider} from "../model/ai-provider";
|
||||
import {appLogger} from "../logging/logger";
|
||||
|
||||
const logger = appLogger.child("ai-provider-queue");
|
||||
|
||||
export type AiRequestQueueTarget = {
|
||||
provider: AiProvider;
|
||||
@@ -7,11 +10,11 @@ export type AiRequestQueueTarget = {
|
||||
baseUrl?: string;
|
||||
};
|
||||
|
||||
type QueueEntry<T> = {
|
||||
type QueueEntry = {
|
||||
target: AiRequestQueueTarget;
|
||||
queueKey: string;
|
||||
run: () => Promise<T>;
|
||||
resolve: (value: T | PromiseLike<T>) => void;
|
||||
run: () => Promise<unknown>;
|
||||
resolve: (value: unknown) => void;
|
||||
reject: (reason?: unknown) => void;
|
||||
onPositionChange: (requestsBefore: number) => Promise<void> | void;
|
||||
signal?: AbortSignal;
|
||||
@@ -26,21 +29,22 @@ type EnqueueOptions<T> = {
|
||||
};
|
||||
|
||||
class AiProviderRequestQueue {
|
||||
private readonly waiting = new Map<string, Array<QueueEntry<any>>>();
|
||||
private readonly waiting = new Map<string, QueueEntry[]>();
|
||||
private readonly active = new Map<string, number>();
|
||||
|
||||
enqueue<T>(target: AiRequestQueueTarget, options: EnqueueOptions<T>): Promise<T> {
|
||||
if (options.signal?.aborted) {
|
||||
logger.debug("enqueue.rejected.aborted", {provider: target.provider, model: target.model, baseUrl: target.baseUrl});
|
||||
return Promise.reject(new Error("Aborted"));
|
||||
}
|
||||
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const queueKey = this.queueKey(target);
|
||||
const entry: QueueEntry<T> = {
|
||||
const entry: QueueEntry = {
|
||||
target,
|
||||
queueKey,
|
||||
run: options.run,
|
||||
resolve,
|
||||
resolve: value => resolve(value as T),
|
||||
reject,
|
||||
onPositionChange: options.onPositionChange,
|
||||
signal: options.signal,
|
||||
@@ -53,21 +57,23 @@ class AiProviderRequestQueue {
|
||||
const removed = this.removeWaitingEntry(entry);
|
||||
if (!removed) return;
|
||||
|
||||
logger.debug("entry.cancelled", {provider: target.provider, model: target.model, baseUrl: target.baseUrl, queueKey});
|
||||
reject(new Error("Aborted"));
|
||||
this.schedule(target);
|
||||
};
|
||||
|
||||
options.signal?.addEventListener("abort", entry.abortHandler, {once: true});
|
||||
this.getOrCreateQueue(queueKey).push(entry);
|
||||
logger.debug("enqueue.accepted", {provider: target.provider, model: target.model, baseUrl: target.baseUrl, queued: this.getOrCreateQueue(queueKey).length, active: this.activeCount(queueKey)});
|
||||
this.schedule(target);
|
||||
});
|
||||
}
|
||||
|
||||
private getQueue(queueKey: string): Array<QueueEntry<any>> | undefined {
|
||||
private getQueue(queueKey: string): QueueEntry[] | undefined {
|
||||
return this.waiting.get(queueKey);
|
||||
}
|
||||
|
||||
private getOrCreateQueue(queueKey: string): Array<QueueEntry<any>> {
|
||||
private getOrCreateQueue(queueKey: string): QueueEntry[] {
|
||||
let queue = this.waiting.get(queueKey);
|
||||
if (!queue) {
|
||||
queue = [];
|
||||
@@ -104,7 +110,7 @@ class AiProviderRequestQueue {
|
||||
]);
|
||||
}
|
||||
|
||||
private removeWaitingEntry(entry: QueueEntry<any>): boolean {
|
||||
private removeWaitingEntry(entry: QueueEntry): boolean {
|
||||
const queue = this.getQueue(entry.queueKey);
|
||||
if (!queue) return false;
|
||||
|
||||
@@ -132,12 +138,14 @@ class AiProviderRequestQueue {
|
||||
}
|
||||
|
||||
if (entry.signal?.aborted) {
|
||||
logger.debug("entry.skipped.aborted", {provider: target.provider, model: target.model, baseUrl: target.baseUrl, queueKey});
|
||||
entry.reject(new Error("Aborted"));
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.started = true;
|
||||
this.setActiveCount(queueKey, this.activeCount(queueKey) + 1);
|
||||
logger.debug("entry.started", {provider: target.provider, model: target.model, baseUrl: target.baseUrl, queued: queue.length, active: this.activeCount(queueKey)});
|
||||
void this.runEntry(entry);
|
||||
}
|
||||
|
||||
@@ -147,10 +155,12 @@ class AiProviderRequestQueue {
|
||||
}
|
||||
}
|
||||
|
||||
private async runEntry(entry: QueueEntry<any>): Promise<void> {
|
||||
private async runEntry(entry: QueueEntry): Promise<void> {
|
||||
try {
|
||||
entry.resolve(await entry.run());
|
||||
logger.debug("entry.done", {provider: entry.target.provider, model: entry.target.model, baseUrl: entry.target.baseUrl});
|
||||
} catch (e) {
|
||||
logger.error("entry.failed", {provider: entry.target.provider, model: entry.target.model, baseUrl: entry.target.baseUrl, error: e});
|
||||
entry.reject(e);
|
||||
} finally {
|
||||
this.setActiveCount(entry.queueKey, this.activeCount(entry.queueKey) - 1);
|
||||
@@ -168,13 +178,13 @@ class AiProviderRequestQueue {
|
||||
})).then(results => {
|
||||
for (const result of results) {
|
||||
if (result.status === "rejected") {
|
||||
console.error(result.reason);
|
||||
logger.error("position_update.failed", {provider: target.provider, model: target.model, reason: result.reason});
|
||||
}
|
||||
}
|
||||
}).catch(console.error);
|
||||
}).catch(error => logger.error("position_updates.failed", {provider: target.provider, model: target.model, error}));
|
||||
}
|
||||
|
||||
private deleteQueueIfIdle(queueKey: string, queue: Array<QueueEntry<any>>): void {
|
||||
private deleteQueueIfIdle(queueKey: string, queue: QueueEntry[]): void {
|
||||
if (!queue.length && this.activeCount(queueKey) <= 0) {
|
||||
this.waiting.delete(queueKey);
|
||||
}
|
||||
|
||||
+12
-10
@@ -198,7 +198,7 @@ async function transcribeGeminiSpeech(audio: AiDownloadedFile, signal?: AbortSig
|
||||
temperature: 0,
|
||||
abortSignal: signal,
|
||||
},
|
||||
});
|
||||
}) as unknown as GeminiSpeechResponse;
|
||||
|
||||
return {
|
||||
provider: AiProvider.GEMINI,
|
||||
@@ -240,17 +240,19 @@ async function transcribeOllamaSpeech(audio: AiDownloadedFile, signal?: AbortSig
|
||||
};
|
||||
}
|
||||
|
||||
function collectGeminiText(response: any): string {
|
||||
if (typeof response?.text === "string") return response.text;
|
||||
type GeminiSpeechResponse = {
|
||||
text?: string;
|
||||
candidates?: Array<{content?: {parts?: Array<{text?: string}>}}> ;
|
||||
};
|
||||
|
||||
const candidates = response?.candidates ?? [];
|
||||
const candidateText = candidates
|
||||
.flatMap((candidate: any) => candidate?.content?.parts ?? [])
|
||||
.map((part: any) => part?.text ?? "")
|
||||
function collectGeminiText(response: GeminiSpeechResponse): string {
|
||||
if (typeof response.text === "string") return response.text;
|
||||
|
||||
const candidateText = (response.candidates ?? [])
|
||||
.flatMap(candidate => candidate.content?.parts ?? [])
|
||||
.map(part => part.text ?? "")
|
||||
.join("");
|
||||
if (candidateText.trim()) return candidateText;
|
||||
|
||||
return (response?.candidates ?? [])
|
||||
.map((output: any) => typeof output === "string" ? output : output?.content?.parts?.[0]?.text ?? "")
|
||||
.join("");
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {StoredAttachment, StoredAttachmentKind} from "../model/stored-attachment
|
||||
import {performFFmpeg} from "../util/ffmpeg";
|
||||
import ffmpeg from "fluent-ffmpeg";
|
||||
import {AsyncSemaphore, KeyedAsyncLock} from "../util/async-lock";
|
||||
import {appLogger} from "../logging/logger";
|
||||
|
||||
export type AiDownloadedFile = {
|
||||
kind: StoredAttachmentKind;
|
||||
@@ -20,6 +21,7 @@ export type AiDownloadedFile = {
|
||||
|
||||
const cachePathLocks = new KeyedAsyncLock();
|
||||
const ffmpegSemaphore = new AsyncSemaphore(2);
|
||||
const logger = appLogger.child("attachments");
|
||||
|
||||
function safeFileName(value: string): string {
|
||||
return value.replace(/[\\/:*?"<>|\u0000-\u001F]/g, "_").slice(0, 180);
|
||||
@@ -90,31 +92,48 @@ function cachePathFor(kind: StoredAttachmentKind, fileUniqueId: string | undefin
|
||||
}
|
||||
|
||||
async function downloadToCache(kind: StoredAttachmentKind, fileId: string, fileName: string, mimeType?: string, fileUniqueId?: string): Promise<StoredAttachment | null> {
|
||||
const startedAt = Date.now();
|
||||
logger.debug("download.start", {kind, fileId, fileName, mimeType});
|
||||
const file = await bot.getFile({file_id: fileId});
|
||||
const finalFileName = fileNameWithExtension(fileName, mimeType, file.file_path);
|
||||
const location = cachePathFor(kind, fileUniqueId, fileId, finalFileName);
|
||||
|
||||
await cachePathLocks.runExclusive(location, async () => {
|
||||
if (fs.existsSync(location)) return;
|
||||
if (fs.existsSync(location)) {
|
||||
logger.trace("download.cache_hit", {kind, location});
|
||||
return;
|
||||
}
|
||||
|
||||
const buffer = await downloadTelegramFile(file.file_path);
|
||||
if (!buffer) return;
|
||||
if (!buffer) {
|
||||
logger.warn("download.empty", {kind, fileId, telegramFilePath: file.file_path});
|
||||
return;
|
||||
}
|
||||
|
||||
const tempLocation = `${location}.${process.pid}.${Date.now()}.tmp`;
|
||||
fs.mkdirSync(path.dirname(location), {recursive: true});
|
||||
fs.writeFileSync(tempLocation, buffer);
|
||||
fs.renameSync(tempLocation, location);
|
||||
logger.debug("download.saved", {kind, location, bytes: buffer.length, duration: logger.duration(startedAt)});
|
||||
});
|
||||
|
||||
return {kind, fileId, fileUniqueId, fileName: finalFileName, mimeType, cachePath: location};
|
||||
}
|
||||
|
||||
async function convertAudioToWav(input: string, output: string, noVideo = false): Promise<void> {
|
||||
const startedAt = Date.now();
|
||||
logger.debug("audio.convert.start", {input, output, noVideo});
|
||||
await cachePathLocks.runExclusive(output, async () => {
|
||||
if (fs.existsSync(output)) return;
|
||||
if (fs.existsSync(output)) {
|
||||
logger.trace("audio.convert.cache_hit", {output});
|
||||
return;
|
||||
}
|
||||
|
||||
await ffmpegSemaphore.runExclusive(async () => {
|
||||
if (fs.existsSync(output)) return;
|
||||
if (fs.existsSync(output)) {
|
||||
logger.trace("audio.convert.cache_hit", {output});
|
||||
return;
|
||||
}
|
||||
|
||||
const tempOutput = `${output}.${process.pid}.${Date.now()}.tmp.wav`;
|
||||
try {
|
||||
@@ -125,14 +144,16 @@ async function convertAudioToWav(input: string, output: string, noVideo = false)
|
||||
.toFormat("wav")
|
||||
.save(tempOutput)
|
||||
.on("progress", (progress) => {
|
||||
console.log("progress", progress);
|
||||
logger.trace("audio.convert.progress", {input, output, progress});
|
||||
});
|
||||
});
|
||||
fs.renameSync(tempOutput, output);
|
||||
logger.debug("audio.convert.done", {input, output, duration: logger.duration(startedAt)});
|
||||
} catch (e) {
|
||||
if (fs.existsSync(tempOutput)) {
|
||||
fs.rmSync(tempOutput, {force: true});
|
||||
}
|
||||
logger.error("audio.convert.failed", {input, output, error: e});
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
@@ -140,7 +161,9 @@ async function convertAudioToWav(input: string, output: string, noVideo = false)
|
||||
}
|
||||
|
||||
export async function cacheMessageAttachments(msg: Message): Promise<StoredAttachment[]> {
|
||||
const startedAt = Date.now();
|
||||
const result: StoredAttachment[] = [];
|
||||
logger.debug("message.cache.start", {chatId: msg.chat?.id, messageId: msg.message_id});
|
||||
|
||||
try {
|
||||
if (msg.photo?.length) {
|
||||
@@ -202,10 +225,12 @@ export async function cacheMessageAttachments(msg: Message): Promise<StoredAttac
|
||||
logError(e);
|
||||
}
|
||||
|
||||
logger.debug("message.cache.done", {chatId: msg.chat?.id, messageId: msg.message_id, attachments: result.length, duration: logger.duration(startedAt)});
|
||||
return result;
|
||||
}
|
||||
|
||||
export function attachmentsToDownloadedFiles(attachments: StoredAttachment[]): AiDownloadedFile[] {
|
||||
logger.trace("downloaded_files.build", {attachments: attachments.length});
|
||||
return attachments
|
||||
.filter(attachment => fs.existsSync(attachment.cachePath))
|
||||
.map(attachment => ({
|
||||
@@ -219,6 +244,7 @@ export function attachmentsToDownloadedFiles(attachments: StoredAttachment[]): A
|
||||
}
|
||||
|
||||
export function cleanupDownloads(files: AiDownloadedFile[]): void {
|
||||
logger.trace("downloaded_files.cleanup", {files: files.length});
|
||||
// Files stay on disk in the message cache; drop in-memory buffers eagerly.
|
||||
for (const file of files) {
|
||||
file.buffer = Buffer.alloc(0);
|
||||
|
||||
@@ -292,7 +292,7 @@ export class TelegramStreamMessage {
|
||||
}
|
||||
if (shouldRemoveKeyboard) await this.removeKeyboard();
|
||||
this.lastSent = next;
|
||||
} catch (e: any) {
|
||||
} catch (e: unknown) {
|
||||
if (shouldRemoveKeyboard && this.isMessageNotModified(e)) {
|
||||
await this.removeKeyboard();
|
||||
this.lastSent = next;
|
||||
@@ -369,8 +369,9 @@ export class TelegramStreamMessage {
|
||||
if (result && result !== true) this.waitMessage = result;
|
||||
this.mediaMode = true;
|
||||
this.lastSent = next;
|
||||
} catch (e: any) {
|
||||
if (!String(e?.message ?? e).includes("message is not modified")) logError(e);
|
||||
} catch (e: unknown) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
if (!message.includes("message is not modified")) logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@ async function synthesizeMistralSpeech(text: string, voice?: string): Promise<Sy
|
||||
if (target.model) request.model = target.model;
|
||||
if (voice || Environment.MISTRAL_TTS_VOICE_ID) request.voiceId = voice || Environment.MISTRAL_TTS_VOICE_ID;
|
||||
|
||||
const response: any = await mistralAi.audio.speech.complete(request);
|
||||
const response = await mistralAi.audio.speech.complete(request) as unknown as {audioData?: string; audio_data?: string};
|
||||
const audioData = response?.audioData ?? response?.audio_data;
|
||||
if (typeof audioData !== "string" || !audioData.trim()) {
|
||||
throw new Error(Environment.mistralTtsNoAudioDataText);
|
||||
@@ -192,7 +192,7 @@ async function synthesizeMistralSpeech(text: string, voice?: string): Promise<Sy
|
||||
async function synthesizeGeminiSpeech(text: string, voice?: string): Promise<SynthesizedSpeech> {
|
||||
const target = resolveAiRuntimeTarget(AiProvider.GEMINI, "textToSpeech");
|
||||
const geminiAi = createGoogleGenAiClient(target);
|
||||
const response: any = await geminiAi.models.generateContent({
|
||||
const response = await geminiAi.models.generateContent({
|
||||
model: target.model,
|
||||
contents: text,
|
||||
config: {
|
||||
|
||||
@@ -15,7 +15,15 @@ export function getOpenAITools(): AiTool[] {
|
||||
}));
|
||||
}
|
||||
|
||||
export function getOpenAIResponsesTools(): any[] {
|
||||
export type OpenAiResponseTool = {
|
||||
type: "function";
|
||||
name: string;
|
||||
description?: string;
|
||||
parameters?: unknown;
|
||||
strict: false;
|
||||
};
|
||||
|
||||
export function getOpenAIResponsesTools(): OpenAiResponseTool[] {
|
||||
return getTools().map(tool => ({
|
||||
type: "function",
|
||||
name: tool.function.name,
|
||||
|
||||
+2
-25
@@ -1,28 +1,5 @@
|
||||
|
||||
/*
|
||||
interface Tool {
|
||||
type: string;
|
||||
function: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
type?: string;
|
||||
parameters?: {
|
||||
type?: string;
|
||||
$defs?: any;
|
||||
items?: any;
|
||||
required?: string[];
|
||||
properties?: {
|
||||
[key: string]: {
|
||||
type?: string | string[];
|
||||
items?: any;
|
||||
description?: string;
|
||||
enum?: any[];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
export type AiToolParameters = {
|
||||
type: "object";
|
||||
@@ -45,7 +22,7 @@ export type AiToolCall = {
|
||||
function: {
|
||||
name: string;
|
||||
arguments: {
|
||||
[key: string]: any;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import axios from "axios";
|
||||
import {toolsLogger} from "./tool-logger";
|
||||
|
||||
const logger = toolsLogger.child("brave-search");
|
||||
import {Environment} from "../../common/environment";
|
||||
import {logError} from "../../util/utils";
|
||||
import {AiTool} from "../tool-types";
|
||||
@@ -264,7 +267,8 @@ function normalizeBraveResultFilter(value: unknown): string {
|
||||
}
|
||||
|
||||
export async function webSearch(args?: Record<string, unknown>) {
|
||||
console.log("braveSearch()");
|
||||
const startedAt = Date.now();
|
||||
logger.info("start", {args});
|
||||
|
||||
try {
|
||||
const query = asNonEmptyString(args?.query);
|
||||
@@ -356,11 +360,12 @@ export async function webSearch(args?: Record<string, unknown>) {
|
||||
|
||||
note: "Use returned URLs as sources. Do not invent facts that are not present in the snippets/results.",
|
||||
};
|
||||
} catch (e: any) {
|
||||
} catch (e: unknown) {
|
||||
logError(e);
|
||||
|
||||
const status = e?.response?.status;
|
||||
const data = e?.response?.data;
|
||||
const axiosLike = e as {response?: {status?: unknown; data?: unknown}};
|
||||
const status = axiosLike.response?.status;
|
||||
const data = axiosLike.response?.data;
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
@@ -369,7 +374,7 @@ export async function webSearch(args?: Record<string, unknown>) {
|
||||
response: data ?? null,
|
||||
};
|
||||
} finally {
|
||||
console.log("END: braveSearch()");
|
||||
logger.debug("done", {duration: logger.duration(startedAt)});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import {AiTool} from "../tool-types";
|
||||
import path from "node:path";
|
||||
import {readFile, writeFile} from "node:fs/promises";
|
||||
import {NOTES_HEADER, notesDir, notesRootFile} from "../../index";
|
||||
import {asNonEmptyString} from "./utils";
|
||||
import fs from "node:fs";
|
||||
import {toolsLogger} from "./tool-logger";
|
||||
|
||||
const logger = toolsLogger.child("create-note");
|
||||
|
||||
export type CreateNoteResult =
|
||||
| { success: true; filePath: string }
|
||||
| { success: false; error: string };
|
||||
|
||||
export const createNoteTool = {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_note",
|
||||
description: "Create a new Markdown note with a valid file name, optional title, and Markdown-formatted content.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
fileName: {
|
||||
type: "string",
|
||||
description: "The valid file name for the note. It must be suitable for use as a file name and must not contain forbidden or unsafe characters such as /, \\, :, *, ?, \", <, >, |, or control characters. Use a clear, concise name based on the note topic. Include the .md extension if the user provides it or if Markdown files are expected."
|
||||
},
|
||||
title: {
|
||||
type: "string",
|
||||
description: "The title of the note. Use a concise, human-readable title based on the user's request or the note content."
|
||||
},
|
||||
content: {
|
||||
type: "string",
|
||||
description: "The full content of the note formatted as valid Markdown. Preserve existing Markdown formatting when provided. If the source content has little or no formatting, add appropriate Markdown structure such as headings, paragraphs, lists, links, code blocks, tables, or emphasis where useful, without changing the meaning."
|
||||
}
|
||||
},
|
||||
required: ["fileName", "content"],
|
||||
}
|
||||
}
|
||||
} satisfies AiTool;
|
||||
|
||||
export async function createNote(
|
||||
args?: Record<string, unknown>
|
||||
): Promise<CreateNoteResult> {
|
||||
const startedAt = Date.now();
|
||||
logger.debug("start", {args});
|
||||
|
||||
const fileName = asNonEmptyString(args?.fileName) ?? "";
|
||||
if (!fileName.trim().length) {
|
||||
return {success: false, error: "No file name provided"};
|
||||
}
|
||||
const title = asNonEmptyString(args?.title) ?? fileName;
|
||||
|
||||
const content = asNonEmptyString(args?.content) ?? "";
|
||||
if (!content.trim().length) {
|
||||
return {success: false, error: "No content provided"};
|
||||
}
|
||||
|
||||
const newFilePath = path.join(notesDir, fileName.endsWith(".md") ? fileName : fileName + ".md");
|
||||
const linkMarkdown = `* [${title}](${path.relative(path.dirname(notesRootFile), newFilePath)})`;
|
||||
|
||||
try {
|
||||
if (fs.existsSync(newFilePath)) {
|
||||
return {success: false, error: "File already exists"};
|
||||
}
|
||||
|
||||
await writeFile(newFilePath, content, "utf-8");
|
||||
|
||||
let rootContent: string;
|
||||
try {
|
||||
rootContent = await readFile(notesRootFile, "utf-8");
|
||||
} catch (e) {
|
||||
rootContent = "";
|
||||
}
|
||||
|
||||
const notesHeaderIndex = rootContent.indexOf(NOTES_HEADER);
|
||||
if (notesHeaderIndex >= 0) {
|
||||
rootContent += "\n" + linkMarkdown;
|
||||
} else {
|
||||
rootContent = NOTES_HEADER + "\n" + linkMarkdown;
|
||||
}
|
||||
|
||||
await writeFile(notesRootFile, rootContent, "utf-8");
|
||||
logger.debug("done", {fileName, filePath: newFilePath, duration: logger.duration(startedAt)});
|
||||
return {success: true, filePath: newFilePath};
|
||||
} catch (error) {
|
||||
logger.error("failed", {duration: logger.duration(startedAt), error});
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {success: false, error: `Failed to process files: ${errorMessage}`};
|
||||
}
|
||||
}
|
||||
@@ -327,8 +327,8 @@ async function assertNoSymlinkInPath(
|
||||
if (stat.isSymbolicLink()) {
|
||||
throw new Error("Symlinks are not allowed in file tool paths.");
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e?.code === "ENOENT" && options?.allowMissingTail) {
|
||||
} catch (e: unknown) {
|
||||
if ((e as NodeJS.ErrnoException).code === "ENOENT" && options?.allowMissingTail) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -341,8 +341,8 @@ async function pathExists(absolutePath: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.promises.lstat(absolutePath);
|
||||
return true;
|
||||
} catch (e: any) {
|
||||
if (e?.code === "ENOENT") return false;
|
||||
} catch (e: unknown) {
|
||||
if ((e as NodeJS.ErrnoException).code === "ENOENT") return false;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
import {AiTool} from "../tool-types";
|
||||
import path from "node:path";
|
||||
import {readdir, readFile, unlink, writeFile} from "node:fs/promises";
|
||||
import {notesDir, notesRootFile} from "../../index";
|
||||
import {asNonEmptyString} from "./utils";
|
||||
import {toolsLogger} from "./tool-logger";
|
||||
|
||||
const logger = toolsLogger.child("notes");
|
||||
|
||||
export type NoteListItem = {
|
||||
fileName: string;
|
||||
filePath: string;
|
||||
relativePath: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export type ListNotesResult =
|
||||
| { success: true; notes: NoteListItem[] }
|
||||
| { success: false; error: string };
|
||||
|
||||
export type GetNoteContentResult =
|
||||
| {
|
||||
success: true;
|
||||
fileName: string;
|
||||
filePath: string;
|
||||
relativePath: string;
|
||||
title: string;
|
||||
content: string;
|
||||
} | { success: false; error: string };
|
||||
|
||||
export const listNotesTool = {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_notes",
|
||||
description: "Display all available Markdown notes from the notes directory.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: [],
|
||||
},
|
||||
},
|
||||
} satisfies AiTool;
|
||||
|
||||
export const getNoteContentTool = {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_note_content",
|
||||
description: "Get the full Markdown content of a specific note by its file name.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
fileName: {
|
||||
type: "string",
|
||||
description:
|
||||
"The file name of the note to read. It may be provided with or without the .md extension. Must not contain forbidden or unsafe characters such as /, \\, :, *, ?, \", <, >, |, or control characters.",
|
||||
},
|
||||
},
|
||||
required: ["fileName"],
|
||||
},
|
||||
},
|
||||
} satisfies AiTool;
|
||||
|
||||
export async function listNotes(): Promise<ListNotesResult> {
|
||||
const startedAt = Date.now();
|
||||
logger.debug("list.start");
|
||||
|
||||
try {
|
||||
const entries = await readdir(notesDir, {withFileTypes: true});
|
||||
|
||||
const markdownFiles = entries
|
||||
.filter((entry) => entry.isFile())
|
||||
.map((entry) => entry.name)
|
||||
.filter((fileName) => fileName.endsWith(".md"));
|
||||
|
||||
const notes: NoteListItem[] = await Promise.all(
|
||||
markdownFiles.map(async (fileName) => {
|
||||
const filePath = path.join(notesDir, fileName);
|
||||
const relativePath = path.relative(path.dirname(notesRootFile), filePath);
|
||||
|
||||
let content = "";
|
||||
try {
|
||||
content = await readFile(filePath, "utf-8");
|
||||
} catch {
|
||||
// Ignore content read errors for individual files.
|
||||
}
|
||||
|
||||
return {
|
||||
fileName,
|
||||
filePath,
|
||||
relativePath,
|
||||
title: extractNoteTitle(fileName, content),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
notes.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
logger.debug("list.done", {notes: notes.length, duration: logger.duration(startedAt)});
|
||||
return {success: true, notes};
|
||||
} catch (error) {
|
||||
logger.error("list.failed", {duration: logger.duration(startedAt), error});
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {success: false, error: `Failed to list notes: ${errorMessage}`};
|
||||
}
|
||||
}
|
||||
|
||||
export async function getNoteContent(
|
||||
args?: Record<string, unknown>,
|
||||
): Promise<GetNoteContentResult> {
|
||||
const startedAt = Date.now();
|
||||
logger.debug("get_content.start", {args});
|
||||
|
||||
const fileName = asNonEmptyString(args?.fileName) ?? "";
|
||||
if (!fileName.trim().length) {
|
||||
return {success: false, error: "No file name provided"};
|
||||
}
|
||||
|
||||
const noteFilePath = buildSafeNoteFilePath(fileName);
|
||||
if (!noteFilePath) {
|
||||
return {success: false, error: "Invalid or unsafe file name provided"};
|
||||
}
|
||||
|
||||
try {
|
||||
const content = await readFile(noteFilePath, "utf-8");
|
||||
const normalizedFileName = path.basename(noteFilePath);
|
||||
const relativePath = path.relative(path.dirname(notesRootFile), noteFilePath);
|
||||
|
||||
logger.debug("get_content.done", {fileName: normalizedFileName, relativePath, chars: content.length, duration: logger.duration(startedAt)});
|
||||
return {
|
||||
success: true,
|
||||
fileName: normalizedFileName,
|
||||
filePath: noteFilePath,
|
||||
relativePath,
|
||||
title: extractNoteTitle(normalizedFileName, content),
|
||||
content,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error("list.failed", {duration: logger.duration(startedAt), error});
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {success: false, error: `Failed to read note: ${errorMessage}`};
|
||||
}
|
||||
}
|
||||
|
||||
function extractNoteTitle(fileName: string, content: string): string {
|
||||
const headingMatch = content.match(/^#\s+(.+)$/m);
|
||||
const heading = headingMatch?.[1]?.trim();
|
||||
|
||||
if (heading) {
|
||||
return heading;
|
||||
}
|
||||
|
||||
return path.basename(fileName, ".md");
|
||||
}
|
||||
|
||||
export function buildSafeNoteFilePath(fileName: string): string | null {
|
||||
const normalizedFileName = fileName.endsWith(".md") ? fileName : `${fileName}.md`;
|
||||
|
||||
if (!normalizedFileName.trim().length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const unsafeFileNamePattern = /[/\\:*?"<>|\x00-\x1F]/;
|
||||
if (unsafeFileNamePattern.test(normalizedFileName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const resolvedNotesDir = path.resolve(notesDir);
|
||||
const resolvedFilePath = path.resolve(notesDir, normalizedFileName);
|
||||
|
||||
if (!resolvedFilePath.startsWith(resolvedNotesDir + path.sep)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return resolvedFilePath;
|
||||
}
|
||||
|
||||
export type UpdateNoteContentResult =
|
||||
| { success: true; filePath: string }
|
||||
| { success: false; error: string };
|
||||
|
||||
export type DeleteNoteResult =
|
||||
| { success: true; filePath: string }
|
||||
| { success: false; error: string };
|
||||
|
||||
export const updateNoteContentTool = {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_note_content",
|
||||
description: "Update the full Markdown content of an existing note by its file name.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
fileName: {
|
||||
type: "string",
|
||||
description:
|
||||
"The file name of the note to update. It may be provided with or without the .md extension. Must not contain forbidden or unsafe characters such as /, \\, :, *, ?, \", <, >, |, or control characters.",
|
||||
},
|
||||
content: {
|
||||
type: "string",
|
||||
description:
|
||||
"The new full content of the note formatted as valid Markdown. This replaces the previous content completely.",
|
||||
},
|
||||
},
|
||||
required: ["fileName", "content"],
|
||||
},
|
||||
},
|
||||
} satisfies AiTool;
|
||||
|
||||
export const deleteNoteTool = {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "delete_note",
|
||||
description: "Delete an existing Markdown note by its file name and remove its link from the notes root file if present.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
fileName: {
|
||||
type: "string",
|
||||
description:
|
||||
"The file name of the note to delete. It may be provided with or without the .md extension. Must not contain forbidden or unsafe characters such as /, \\, :, *, ?, \", <, >, |, or control characters.",
|
||||
},
|
||||
},
|
||||
required: ["fileName"],
|
||||
},
|
||||
},
|
||||
} satisfies AiTool;
|
||||
|
||||
export async function updateNoteContent(
|
||||
args?: Record<string, unknown>,
|
||||
): Promise<UpdateNoteContentResult> {
|
||||
const startedAt = Date.now();
|
||||
logger.debug("update_content.start", {args});
|
||||
|
||||
const fileName = asNonEmptyString(args?.fileName) ?? "";
|
||||
if (!fileName.trim().length) {
|
||||
return {success: false, error: "No file name provided"};
|
||||
}
|
||||
|
||||
const content = asNonEmptyString(args?.content) ?? "";
|
||||
if (!content.trim().length) {
|
||||
return {success: false, error: "No content provided"};
|
||||
}
|
||||
|
||||
const noteFilePath = buildSafeNoteFilePath(fileName);
|
||||
if (!noteFilePath) {
|
||||
return {success: false, error: "Invalid or unsafe file name provided"};
|
||||
}
|
||||
|
||||
try {
|
||||
await readFile(noteFilePath, "utf-8");
|
||||
await writeFile(noteFilePath, content, "utf-8");
|
||||
logger.debug("update_content.done", {fileName, filePath: noteFilePath, chars: content.length, duration: logger.duration(startedAt)});
|
||||
|
||||
return {success: true, filePath: noteFilePath};
|
||||
} catch (error) {
|
||||
logger.error("list.failed", {duration: logger.duration(startedAt), error});
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {success: false, error: `Failed to update note: ${errorMessage}`};
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteNote(
|
||||
args?: Record<string, unknown>,
|
||||
): Promise<DeleteNoteResult> {
|
||||
const startedAt = Date.now();
|
||||
logger.debug("delete.start", {args});
|
||||
|
||||
const fileName = asNonEmptyString(args?.fileName) ?? "";
|
||||
if (!fileName.trim().length) {
|
||||
return {success: false, error: "No file name provided"};
|
||||
}
|
||||
|
||||
const noteFilePath = buildSafeNoteFilePath(fileName);
|
||||
if (!noteFilePath) {
|
||||
return {success: false, error: "Invalid or unsafe file name provided"};
|
||||
}
|
||||
|
||||
try {
|
||||
await unlink(noteFilePath);
|
||||
await removeNoteLinkFromRoot(noteFilePath);
|
||||
logger.debug("delete.done", {fileName, filePath: noteFilePath, duration: logger.duration(startedAt)});
|
||||
|
||||
return {success: true, filePath: noteFilePath};
|
||||
} catch (error) {
|
||||
logger.error("list.failed", {duration: logger.duration(startedAt), error});
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {success: false, error: `Failed to delete note: ${errorMessage}`};
|
||||
}
|
||||
}
|
||||
|
||||
async function removeNoteLinkFromRoot(noteFilePath: string): Promise<void> {
|
||||
let rootContent: string;
|
||||
|
||||
try {
|
||||
rootContent = await readFile(notesRootFile, "utf-8");
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const relativePath = path.relative(path.dirname(notesRootFile), noteFilePath);
|
||||
const normalizedRelativePath = relativePath.replaceAll("\\", "\\\\");
|
||||
|
||||
const escapedRelativePath = escapeRegExp(normalizedRelativePath);
|
||||
const linkLinePattern = new RegExp(
|
||||
`^\\s*[-*]\\s+\\[[^\\]]+]\\(${escapedRelativePath}\\)\\s*$\\n?`,
|
||||
"gm",
|
||||
);
|
||||
|
||||
const updatedRootContent = rootContent.replace(linkLinePattern, "");
|
||||
|
||||
if (updatedRootContent !== rootContent) {
|
||||
await writeFile(notesRootFile, updatedRootContent.trimEnd() + "\n", "utf-8");
|
||||
}
|
||||
}
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import {AiTool} from "../tool-types";
|
||||
import axios from "axios";
|
||||
import {toolsLogger} from "./tool-logger";
|
||||
|
||||
const logger = toolsLogger.child("market-rates");
|
||||
|
||||
export const getMarketRatesTool = {
|
||||
type: "function",
|
||||
@@ -58,12 +61,15 @@ export const marketRatesToolPrompt = [
|
||||
"- When the user asks for all rates, group fiat currencies separately from crypto and gold.",
|
||||
].join("\n");
|
||||
|
||||
export async function getMarketRates(): Promise<any | undefined> {
|
||||
export async function getMarketRates(): Promise<unknown | undefined> {
|
||||
const startedAt = Date.now();
|
||||
try {
|
||||
logger.info("start");
|
||||
const response = await axios.get("https://apid.r00t.top/api/v2/currency/rates");
|
||||
logger.debug("done", {duration: logger.duration(startedAt), status: response.status});
|
||||
return response.data;
|
||||
} catch (e: any) {
|
||||
console.error("GET_MARKET_RATES", e);
|
||||
} catch (e: unknown) {
|
||||
logger.error("failed", {duration: logger.duration(startedAt), error: e});
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,9 @@ import path from "node:path";
|
||||
import {AiTool} from "../tool-types";
|
||||
import {Environment} from "../../common/environment";
|
||||
import {randomUUID} from "node:crypto";
|
||||
import {toolsLogger} from "./tool-logger";
|
||||
|
||||
const logger = toolsLogger.child("python-interpreter");
|
||||
|
||||
export const PYTHON_INTERPRETER_TOOL_NAME = "python_interpreter";
|
||||
|
||||
@@ -203,17 +206,17 @@ export async function runPythonInterpreter(
|
||||
};
|
||||
}
|
||||
|
||||
console.time("python.syntax");
|
||||
const syntaxStartedAt = Date.now();
|
||||
const syntax = await validatePythonSyntax(args.code, options);
|
||||
console.timeEnd("python.syntax");
|
||||
logger.debug("syntax.done", {duration: logger.duration(syntaxStartedAt), ok: syntax.ok});
|
||||
|
||||
if (!syntax.ok) {
|
||||
return syntax;
|
||||
}
|
||||
|
||||
console.time("python.execution");
|
||||
const executionStartedAt = Date.now();
|
||||
const result = await executePythonCode(args, options);
|
||||
console.timeEnd("python.execution");
|
||||
logger.debug("execution.done", {duration: logger.duration(executionStartedAt), ok: result.ok, phase: result.phase});
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -293,7 +296,8 @@ async function executePythonCode(
|
||||
args: PythonInterpreterArgs,
|
||||
options: PythonInterpreterOptions = {},
|
||||
): Promise<PythonToolResult> {
|
||||
console.log("EXECUTE_PYTHON_CODE", "ARGS: ", JSON.stringify(args), "; OPTIONS: ", JSON.stringify(options));
|
||||
const startedAt = Date.now();
|
||||
logger.info("execute.start", {args, options});
|
||||
|
||||
const pythonBinary =
|
||||
options.pythonBinary ?? process.env.PYTHON_INTERPRETER_BINARY ?? "C:\\Users\\meloda\\Desktop\\AI_BOT\\.venv\\Scripts\\python.exe";
|
||||
@@ -329,7 +333,7 @@ async function executePythonCode(
|
||||
mode: 0o600,
|
||||
});
|
||||
|
||||
console.log("EXECUTE_PYTHON_CODE", "SCRIPT FILE WRITTEN", new Date());
|
||||
logger.debug("script.written", {tempDir, userScriptPath, runnerPath, duration: logger.duration(startedAt)});
|
||||
|
||||
const result = await runProcess({
|
||||
command: pythonBinary,
|
||||
@@ -346,10 +350,10 @@ async function executePythonCode(
|
||||
},
|
||||
});
|
||||
|
||||
console.log("EXECUTE_PYTHON_CODE", "RESULT ACHIEVED", new Date());
|
||||
logger.debug("process.done", {duration: logger.duration(startedAt), exitCode: result.exitCode, timedOut: result.timedOut, outputTruncated: result.outputTruncated});
|
||||
|
||||
if (result.timedOut) {
|
||||
console.log("EXECUTE_PYTHON_CODE", "RESULT ERROR TIMED OUT", new Date());
|
||||
logger.warn("process.timeout", {duration: logger.duration(startedAt)});
|
||||
return {
|
||||
ok: false,
|
||||
phase: "execution",
|
||||
@@ -365,7 +369,7 @@ async function executePythonCode(
|
||||
}
|
||||
|
||||
if (result.outputTruncated) {
|
||||
console.log("EXECUTE_PYTHON_CODE", "RESULT ERROR TRUNCATED", new Date());
|
||||
logger.warn("process.output_truncated", {duration: logger.duration(startedAt), stdoutChars: result.stdout.length, stderrChars: result.stderr.length});
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
@@ -382,7 +386,7 @@ async function executePythonCode(
|
||||
}
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
console.log("EXECUTE_PYTHON_CODE", "RESULT ERROR EXIT CODE", new Date(), "\n", JSON.stringify(result, null, 2));
|
||||
logger.warn("process.non_zero_exit", {duration: logger.duration(startedAt), result});
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
@@ -398,7 +402,7 @@ async function executePythonCode(
|
||||
};
|
||||
}
|
||||
|
||||
console.log("EXECUTE_PYTHON_CODE", "RESULT NORMAL", new Date());
|
||||
logger.debug("process.ok", {duration: logger.duration(startedAt)});
|
||||
|
||||
const {
|
||||
artifacts,
|
||||
@@ -420,7 +424,7 @@ async function executePythonCode(
|
||||
skippedArtifacts,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log("EXECUTE_PYTHON_CODE", "RESULT ERROR", new Date());
|
||||
logger.error("execute.failed", {duration: logger.duration(startedAt), error});
|
||||
return {
|
||||
ok: false,
|
||||
phase: "internal",
|
||||
@@ -692,7 +696,7 @@ function parsePythonInterpreterArgs(
|
||||
|
||||
return {
|
||||
code,
|
||||
stdin,
|
||||
stdin: typeof stdin === "string" ? stdin : undefined,
|
||||
timeoutMs: timeoutMs === undefined ? undefined : Number(timeoutMs),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25,11 +25,31 @@ import {
|
||||
updateFile,
|
||||
updateFileTool
|
||||
} from "./file-system";
|
||||
import {createNote, createNoteTool} from "./create-note";
|
||||
import {
|
||||
deleteNote,
|
||||
deleteNoteTool,
|
||||
getNoteContent,
|
||||
getNoteContentTool,
|
||||
listNotes,
|
||||
listNotesTool,
|
||||
updateNoteContent,
|
||||
updateNoteContentTool
|
||||
} from "./list-notes";
|
||||
import {getNoteFile, getNoteFileTool} from "./send-note-file";
|
||||
import {searchNotes, searchNotesTool} from "./search-notes";
|
||||
|
||||
export const getTools = () => {
|
||||
const tools: AiTool[] = [
|
||||
getCurrentDateTimeTool,
|
||||
getMarketRatesTool,
|
||||
createNoteTool,
|
||||
listNotesTool,
|
||||
getNoteContentTool,
|
||||
updateNoteContentTool,
|
||||
deleteNoteTool,
|
||||
getNoteFileTool,
|
||||
searchNotesTool
|
||||
];
|
||||
|
||||
if (Environment.ENABLE_PYTHON_INTERPRETER) {
|
||||
@@ -61,13 +81,30 @@ export const getTools = () => {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return tools;
|
||||
// return [
|
||||
// createNoteTool,
|
||||
// listNotesTool,
|
||||
// getNoteContentTool,
|
||||
// updateNoteContentTool,
|
||||
// deleteNoteTool,
|
||||
// getNoteFileTool,
|
||||
// searchNotesTool
|
||||
// ];
|
||||
};
|
||||
|
||||
export const getToolHandlers = () => {
|
||||
let handlers: Record<string, ToolHandler> = {
|
||||
get_datetime: getCurrentDateTime,
|
||||
get_market_rates: getMarketRates,
|
||||
create_note: createNote,
|
||||
list_notes: listNotes,
|
||||
get_note_content: getNoteContent,
|
||||
update_note_content: updateNoteContent,
|
||||
delete_note: deleteNote,
|
||||
get_note_file: getNoteFile,
|
||||
search_notes: searchNotes
|
||||
};
|
||||
|
||||
if (Environment.ENABLE_PYTHON_INTERPRETER) {
|
||||
|
||||
+10
-2
@@ -1,6 +1,9 @@
|
||||
import {getToolHandlers} from "./registry";
|
||||
import {normalizeToolArguments} from "./utils";
|
||||
import {PYTHON_INTERPRETER_TOOL_NAME, PythonInterpreterInputFile, runPythonInterpreter} from "./python-interpretator";
|
||||
import {toolsLogger} from "./tool-logger";
|
||||
|
||||
const logger = toolsLogger.child("runtime");
|
||||
|
||||
export type ToolRuntimeContext = {
|
||||
pythonInputFiles?: PythonInterpreterInputFile[];
|
||||
@@ -16,7 +19,9 @@ export async function executeToolCall(
|
||||
args?: unknown,
|
||||
context: ToolRuntimeContext = {},
|
||||
): Promise<string> {
|
||||
const startedAt = Date.now();
|
||||
const handler = getToolHandlers()[name];
|
||||
logger.info("execute.start", {name, args});
|
||||
|
||||
if (!handler) {
|
||||
return stringifyToolResult({
|
||||
@@ -35,14 +40,17 @@ export async function executeToolCall(
|
||||
});
|
||||
|
||||
const s = stringifyToolResult(result);
|
||||
console.log("PYTHON_INTERPRETER_STRING_RESULT", s);
|
||||
logger.debug("execute.done", {name, chars: s.length, duration: logger.duration(startedAt)});
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
const result = await handler(normalizeToolArguments(args));
|
||||
return stringifyToolResult(result);
|
||||
const s = stringifyToolResult(result);
|
||||
logger.debug("execute.done", {name, chars: s.length, duration: logger.duration(startedAt)});
|
||||
return s;
|
||||
} catch (error) {
|
||||
logger.error("execute.failed", {name, duration: logger.duration(startedAt), error});
|
||||
return stringifyToolResult({
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
import {AiTool} from "../tool-types";
|
||||
import path from "node:path";
|
||||
import {readdir, readFile} from "node:fs/promises";
|
||||
import {notesDir, notesRootFile} from "../../index";
|
||||
import {asNonEmptyString} from "./utils";
|
||||
import {toolsLogger} from "./tool-logger";
|
||||
|
||||
const logger = toolsLogger.child("search-notes");
|
||||
|
||||
export type SearchNoteMatchedField = "file_name" | "title" | "content";
|
||||
|
||||
export type SearchNoteItem = {
|
||||
fileName: string;
|
||||
filePath: string;
|
||||
relativePath: string;
|
||||
title: string;
|
||||
score: number;
|
||||
matchedFields: SearchNoteMatchedField[];
|
||||
snippet?: string;
|
||||
};
|
||||
|
||||
export type SearchNotesResult =
|
||||
| { success: true; results: SearchNoteItem[] }
|
||||
| { success: false; error: string };
|
||||
|
||||
export const searchNotesTool = {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "search_notes",
|
||||
description:
|
||||
"Search Markdown notes by file name, note title, and full note content. Supports fuzzy matching. Use this when the user refers to a note by title, topic, partial title, approximate name, keyword, or something written inside the note. Returns success=true and results[], where each result contains fileName, title, score, matchedFields, relativePath, and optional snippet. Later note tools should use results[0].fileName unless multiple results are ambiguous.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: {
|
||||
type: "string",
|
||||
description:
|
||||
"Search query for finding notes by file name, title, topic, keywords, or content. Can be partial, approximate, or contain typos. Use a short clean phrase, not the full user sentence.",
|
||||
},
|
||||
limit: {
|
||||
type: "integer",
|
||||
description:
|
||||
"Maximum number of search results to return. Defaults to 3. Maximum is 10.",
|
||||
minimum: 1,
|
||||
maximum: 10,
|
||||
default: 3,
|
||||
},
|
||||
},
|
||||
required: ["query"],
|
||||
},
|
||||
},
|
||||
} satisfies AiTool;
|
||||
|
||||
export async function searchNotes(
|
||||
args?: Record<string, unknown>,
|
||||
): Promise<SearchNotesResult> {
|
||||
const startedAt = Date.now();
|
||||
logger.debug("start", {args});
|
||||
|
||||
const query = asNonEmptyString(args?.query) ?? "";
|
||||
if (!query.trim().length) {
|
||||
return {success: false, error: "No query provided"};
|
||||
}
|
||||
|
||||
const limit = parseSearchLimit(args?.limit);
|
||||
|
||||
try {
|
||||
const entries = await readdir(notesDir, {withFileTypes: true});
|
||||
|
||||
const markdownFiles = entries
|
||||
.filter((entry) => entry.isFile())
|
||||
.map((entry) => entry.name)
|
||||
.filter((fileName) => fileName.endsWith(".md"));
|
||||
|
||||
const notes = await Promise.all(
|
||||
markdownFiles.map(async (fileName) => {
|
||||
const filePath = path.join(notesDir, fileName);
|
||||
const relativePath = path.relative(path.dirname(notesRootFile), filePath);
|
||||
|
||||
let content = "";
|
||||
try {
|
||||
content = await readFile(filePath, "utf-8");
|
||||
} catch {
|
||||
// Ignore content read errors for individual files.
|
||||
}
|
||||
|
||||
const title = extractNoteTitle(fileName, content);
|
||||
const fileNameWithoutExtension = path.basename(fileName, ".md");
|
||||
|
||||
const fileNameScore = calculateFuzzyScore(query, fileNameWithoutExtension);
|
||||
const titleScore = calculateFuzzyScore(query, title);
|
||||
const contentScore = calculateContentScore(query, content);
|
||||
|
||||
const matchedFields: SearchNoteMatchedField[] = [];
|
||||
|
||||
if (fileNameScore > 0) {
|
||||
matchedFields.push("file_name");
|
||||
}
|
||||
|
||||
if (titleScore > 0) {
|
||||
matchedFields.push("title");
|
||||
}
|
||||
|
||||
if (contentScore > 0) {
|
||||
matchedFields.push("content");
|
||||
}
|
||||
|
||||
const score = Math.max(
|
||||
fileNameScore,
|
||||
titleScore,
|
||||
contentScore,
|
||||
);
|
||||
|
||||
return {
|
||||
fileName,
|
||||
filePath,
|
||||
relativePath,
|
||||
title,
|
||||
score,
|
||||
matchedFields,
|
||||
snippet:
|
||||
contentScore > 0
|
||||
? buildContentSnippet(query, content)
|
||||
: undefined,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const results = notes
|
||||
.filter((note) => note.score > 0)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, limit);
|
||||
|
||||
logger.debug("done", {query, limit, results: results.length, duration: logger.duration(startedAt)});
|
||||
return {success: true, results};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {success: false, error: `Failed to search notes: ${errorMessage}`};
|
||||
}
|
||||
}
|
||||
|
||||
function parseSearchLimit(value: unknown): number {
|
||||
const parsed =
|
||||
typeof value === "number"
|
||||
? value
|
||||
: typeof value === "string"
|
||||
? Number.parseInt(value, 10)
|
||||
: 3;
|
||||
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
return Math.max(1, Math.min(10, Math.floor(parsed)));
|
||||
}
|
||||
|
||||
function extractNoteTitle(fileName: string, content: string): string {
|
||||
const headingMatch = content.match(/^#\s+(.+)$/m);
|
||||
const heading = headingMatch?.[1]?.trim();
|
||||
|
||||
if (heading) {
|
||||
return heading;
|
||||
}
|
||||
|
||||
return path.basename(fileName, ".md");
|
||||
}
|
||||
|
||||
function calculateFuzzyScore(query: string, value: string): number {
|
||||
const normalizedQuery = normalizeSearchText(query);
|
||||
const normalizedValue = normalizeSearchText(value);
|
||||
|
||||
if (!normalizedQuery.length || !normalizedValue.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (normalizedValue === normalizedQuery) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
if (normalizedValue.startsWith(normalizedQuery)) {
|
||||
return 90;
|
||||
}
|
||||
|
||||
if (normalizedValue.includes(normalizedQuery)) {
|
||||
return 85;
|
||||
}
|
||||
|
||||
const queryWords = normalizedQuery.split(" ").filter(Boolean);
|
||||
const valueWords = normalizedValue.split(" ").filter(Boolean);
|
||||
|
||||
const wordMatchScore = calculateWordMatchScore(queryWords, valueWords);
|
||||
const subsequenceScore = isSubsequence(normalizedQuery, normalizedValue) ? 55 : 0;
|
||||
const distanceScore = calculateLevenshteinScore(normalizedQuery, normalizedValue);
|
||||
|
||||
return Math.max(wordMatchScore, subsequenceScore, distanceScore);
|
||||
}
|
||||
|
||||
function calculateContentScore(query: string, content: string): number {
|
||||
const normalizedQuery = normalizeSearchText(query);
|
||||
const normalizedContent = normalizeSearchText(content);
|
||||
|
||||
if (!normalizedQuery.length || !normalizedContent.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (normalizedContent.includes(normalizedQuery)) {
|
||||
return 70;
|
||||
}
|
||||
|
||||
const queryWords = normalizedQuery.split(" ").filter(Boolean);
|
||||
const contentWords = new Set(normalizedContent.split(" ").filter(Boolean));
|
||||
|
||||
if (!queryWords.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let matchedWords = 0;
|
||||
|
||||
for (const queryWord of queryWords) {
|
||||
if (contentWords.has(queryWord)) {
|
||||
matchedWords++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const hasPartialMatch = [...contentWords].some((contentWord) => {
|
||||
if (contentWord.includes(queryWord) || queryWord.includes(contentWord)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (queryWord.length < 4 || contentWord.length < 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const distance = levenshteinDistance(queryWord, contentWord);
|
||||
const maxLength = Math.max(queryWord.length, contentWord.length);
|
||||
const similarity = 1 - distance / maxLength;
|
||||
|
||||
return similarity >= 0.75;
|
||||
});
|
||||
|
||||
if (hasPartialMatch) {
|
||||
matchedWords += 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
const matchRatio = matchedWords / queryWords.length;
|
||||
|
||||
if (matchRatio <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.round(matchRatio * 60);
|
||||
}
|
||||
|
||||
function normalizeSearchText(value: string): string {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.normalize("NFKD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/ё/g, "е")
|
||||
.replace(/[^a-zа-я0-9\s-]/gi, " ")
|
||||
.replace(/[-_]+/g, " ")
|
||||
.replace(/\s+/g, " ");
|
||||
}
|
||||
|
||||
function calculateWordMatchScore(queryWords: string[], valueWords: string[]): number {
|
||||
if (!queryWords.length || !valueWords.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let matchedWords = 0;
|
||||
|
||||
for (const queryWord of queryWords) {
|
||||
const bestWordScore = Math.max(
|
||||
...valueWords.map((valueWord) => {
|
||||
if (valueWord === queryWord) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (valueWord.startsWith(queryWord) || valueWord.includes(queryWord)) {
|
||||
return 0.85;
|
||||
}
|
||||
|
||||
const distance = levenshteinDistance(queryWord, valueWord);
|
||||
const maxLength = Math.max(queryWord.length, valueWord.length);
|
||||
const similarity = 1 - distance / maxLength;
|
||||
|
||||
return similarity >= 0.7 ? similarity : 0;
|
||||
}),
|
||||
);
|
||||
|
||||
if (bestWordScore > 0) {
|
||||
matchedWords += bestWordScore;
|
||||
}
|
||||
}
|
||||
|
||||
const ratio = matchedWords / queryWords.length;
|
||||
return Math.round(ratio * 75);
|
||||
}
|
||||
|
||||
function calculateLevenshteinScore(query: string, value: string): number {
|
||||
const distance = levenshteinDistance(query, value);
|
||||
const maxLength = Math.max(query.length, value.length);
|
||||
|
||||
if (maxLength === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const similarity = 1 - distance / maxLength;
|
||||
|
||||
if (similarity < 0.45) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.round(similarity * 65);
|
||||
}
|
||||
|
||||
function isSubsequence(query: string, value: string): boolean {
|
||||
let queryIndex = 0;
|
||||
|
||||
for (const valueChar of value) {
|
||||
if (valueChar === query[queryIndex]) {
|
||||
queryIndex++;
|
||||
}
|
||||
|
||||
if (queryIndex === query.length) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function levenshteinDistance(a: string, b: string): number {
|
||||
const matrix: number[][] = Array.from({length: a.length + 1}, () =>
|
||||
Array.from({length: b.length + 1}, () => 0),
|
||||
);
|
||||
|
||||
for (let i = 0; i <= a.length; i++) {
|
||||
matrix[i][0] = i;
|
||||
}
|
||||
|
||||
for (let j = 0; j <= b.length; j++) {
|
||||
matrix[0][j] = j;
|
||||
}
|
||||
|
||||
for (let i = 1; i <= a.length; i++) {
|
||||
for (let j = 1; j <= b.length; j++) {
|
||||
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
||||
|
||||
matrix[i][j] = Math.min(
|
||||
matrix[i - 1][j] + 1,
|
||||
matrix[i][j - 1] + 1,
|
||||
matrix[i - 1][j - 1] + cost,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return matrix[a.length][b.length];
|
||||
}
|
||||
|
||||
function buildContentSnippet(query: string, content: string): string | undefined {
|
||||
const normalizedQuery = query.trim().toLowerCase();
|
||||
const normalizedContent = content.toLowerCase();
|
||||
|
||||
let matchIndex = normalizedContent.indexOf(normalizedQuery);
|
||||
|
||||
if (matchIndex < 0) {
|
||||
const queryWords = normalizeSearchText(query)
|
||||
.split(" ")
|
||||
.filter((word) => word.length >= 3);
|
||||
|
||||
for (const word of queryWords) {
|
||||
matchIndex = normalizedContent.indexOf(word);
|
||||
if (matchIndex >= 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matchIndex < 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const snippetRadius = 120;
|
||||
const start = Math.max(0, matchIndex - snippetRadius);
|
||||
const end = Math.min(content.length, matchIndex + normalizedQuery.length + snippetRadius);
|
||||
|
||||
const prefix = start > 0 ? "..." : "";
|
||||
const suffix = end < content.length ? "..." : "";
|
||||
|
||||
return `${prefix}${content.slice(start, end).replace(/\s+/g, " ").trim()}${suffix}`;
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import {AiTool} from "../tool-types";
|
||||
import path from "node:path";
|
||||
import {readFile, stat} from "node:fs/promises";
|
||||
import {notesRootFile} from "../../index";
|
||||
import {asNonEmptyString} from "./utils";
|
||||
import {buildSafeNoteFilePath} from "./list-notes";
|
||||
import z from "zod";
|
||||
import {toolsLogger} from "./tool-logger";
|
||||
|
||||
const logger = toolsLogger.child("get-note-file");
|
||||
|
||||
export type NoteFileAttachment = {
|
||||
type: "local_file";
|
||||
fileName: string;
|
||||
// filePath: string;
|
||||
relativePath: string;
|
||||
mimeType: "text/markdown";
|
||||
sizeBytes: number;
|
||||
};
|
||||
|
||||
export type GetNoteFileResult =
|
||||
| {
|
||||
success: true;
|
||||
attachment: NoteFileAttachment;
|
||||
} | { success: false; error: string };
|
||||
|
||||
export const NoteFileAttachmentSchema = z.object({
|
||||
type: z.literal("local_file"),
|
||||
fileName: z.string(),
|
||||
// filePath: z.string(),
|
||||
relativePath: z.string(),
|
||||
mimeType: z.literal("text/markdown"),
|
||||
sizeBytes: z.number(),
|
||||
});
|
||||
|
||||
export const GetNoteFileResultSchema = z.discriminatedUnion("success", [
|
||||
z.object({
|
||||
success: z.literal(true),
|
||||
attachment: NoteFileAttachmentSchema,
|
||||
}),
|
||||
z.object({
|
||||
success: z.literal(false),
|
||||
error: z.string(),
|
||||
}),
|
||||
]);
|
||||
|
||||
export const getNoteFileTool = {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_note_file",
|
||||
description:
|
||||
"Prepare a Markdown note file to be sent to the user as a .md attachment. Returns a local file descriptor that the host application should use to upload or send the file.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
fileName: {
|
||||
type: "string",
|
||||
description:
|
||||
"The file name of the note to send. It may be provided with or without the .md extension. Must not contain forbidden or unsafe characters such as /, \\, :, *, ?, \", <, >, |, or control characters.",
|
||||
},
|
||||
},
|
||||
required: ["fileName"],
|
||||
},
|
||||
},
|
||||
} satisfies AiTool;
|
||||
|
||||
export async function getNoteFile(
|
||||
args?: Record<string, unknown>,
|
||||
): Promise<GetNoteFileResult> {
|
||||
logger.debug("start", {args});
|
||||
|
||||
const fileName = asNonEmptyString(args?.fileName) ?? "";
|
||||
if (!fileName.trim().length) {
|
||||
return {success: false, error: "No file name provided"};
|
||||
}
|
||||
|
||||
const noteFilePath = buildSafeNoteFilePath(fileName);
|
||||
if (!noteFilePath) {
|
||||
return {success: false, error: "Invalid or unsafe file name provided"};
|
||||
}
|
||||
|
||||
try {
|
||||
// Проверяем, что файл существует и действительно читается.
|
||||
await readFile(noteFilePath, "utf-8");
|
||||
|
||||
const fileStat = await stat(noteFilePath);
|
||||
if (!fileStat.isFile()) {
|
||||
return {success: false, error: "Note path is not a file"};
|
||||
}
|
||||
|
||||
const normalizedFileName = path.basename(noteFilePath);
|
||||
const relativePath = path.relative(path.dirname(notesRootFile), noteFilePath);
|
||||
|
||||
const result: GetNoteFileResult = {
|
||||
success: true,
|
||||
attachment: {
|
||||
type: "local_file",
|
||||
fileName: normalizedFileName,
|
||||
// filePath: noteFilePath,
|
||||
relativePath,
|
||||
mimeType: "text/markdown",
|
||||
sizeBytes: fileStat.size,
|
||||
},
|
||||
};
|
||||
|
||||
logger.debug("done", {fileName: result.attachment.fileName, relativePath: result.attachment.relativePath, sizeBytes: result.attachment.sizeBytes});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {success: false, error: `Failed to prepare note file: ${errorMessage}`};
|
||||
}
|
||||
}
|
||||
+32
-4
@@ -1,4 +1,8 @@
|
||||
import {Ollama} from "ollama";
|
||||
import {z} from "zod";
|
||||
import {toolsLogger} from "./tool-logger";
|
||||
|
||||
const logger = toolsLogger.child("utils");
|
||||
|
||||
export function asNonEmptyString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim().length > 0
|
||||
@@ -78,14 +82,15 @@ export async function unloadAllOllamaModels(ollama: Ollama, exceptFor?: string[]
|
||||
);
|
||||
|
||||
await Promise.all(unloadPromises);
|
||||
console.log("All models have been requested to unload" + exceptFor?.length ? ` except for [${exceptFor?.join(", ")}].` : ".");
|
||||
logger.info("ollama.unload_all.done", {count: modelsToUnload.length, exceptFor});
|
||||
} catch (error) {
|
||||
console.error("Error unloading models:", error);
|
||||
logger.error("ollama.unload_all.failed", {exceptFor, error});
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadOllamaModel(model: string, ollama: Ollama, contextLength: number): Promise<boolean> {
|
||||
try {
|
||||
logger.info("ollama.load.start", {model, contextLength});
|
||||
await ollama.generate({
|
||||
model: model,
|
||||
stream: false,
|
||||
@@ -94,9 +99,32 @@ export async function loadOllamaModel(model: string, ollama: Ollama, contextLeng
|
||||
num_ctx: contextLength
|
||||
}
|
||||
});
|
||||
logger.info("ollama.load.done", {model, contextLength});
|
||||
return true;
|
||||
} catch (e: any) {
|
||||
console.error("Error loading Ollama model:", model);
|
||||
} catch (e: unknown) {
|
||||
logger.error("ollama.load.failed", {model, contextLength, error: e});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export type ToolPlanStep = {
|
||||
t: string;
|
||||
h: string;
|
||||
from: string;
|
||||
};
|
||||
|
||||
export type RouterPlan = {
|
||||
s: ToolPlanStep[];
|
||||
m: string;
|
||||
};
|
||||
|
||||
export const ToolPlanStepSchema = z.object({
|
||||
t: z.string(),
|
||||
h: z.string(),
|
||||
from: z.string(),
|
||||
});
|
||||
|
||||
export const RouterPlanSchema = z.object({
|
||||
s: z.array(ToolPlanStepSchema),
|
||||
m: z.string()
|
||||
});
|
||||
+11
-6
@@ -1,4 +1,7 @@
|
||||
import axios from "axios";
|
||||
import {toolsLogger} from "./tool-logger";
|
||||
|
||||
const logger = toolsLogger.child("weather");
|
||||
import {Environment} from "../../common/environment";
|
||||
import {logError} from "../../util/utils";
|
||||
import {AiTool} from "../tool-types";
|
||||
@@ -42,8 +45,9 @@ export const weatherToolPrompt = [
|
||||
"If the city is missing or unclear, ask the user to specify it.",
|
||||
].join("\n");
|
||||
|
||||
export async function getWeather(args?: Record<string, unknown>): Promise<any | null> {
|
||||
console.log("getWeather()");
|
||||
export async function getWeather(args?: Record<string, unknown>): Promise<Record<string, unknown> | null> {
|
||||
const startedAt = Date.now();
|
||||
logger.info("start", {args});
|
||||
try {
|
||||
const city = asNonEmptyString(args?.city);
|
||||
const lang = asNonEmptyString(args?.lang);
|
||||
@@ -61,7 +65,7 @@ export async function getWeather(args?: Record<string, unknown>): Promise<any |
|
||||
appid: apiKey,
|
||||
},
|
||||
})).data[0];
|
||||
console.log("GEOCODE_RESPONSE", geocodeResponse);
|
||||
logger.debug("geocode.done", {city, country: geocodeResponse?.country, hasResult: !!geocodeResponse, geocodeResponse});
|
||||
if (!geocodeResponse) {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -83,7 +87,7 @@ export async function getWeather(args?: Record<string, unknown>): Promise<any |
|
||||
...(lang ? {lang} : {}),
|
||||
},
|
||||
})).data;
|
||||
console.log("RESPONSE: getWeather(lang=" + lang + "): ", response);
|
||||
logger.debug("weather_api.done", {city, country: geocodeResponse.country, lang, units: "metric", hasResponse: !!response});
|
||||
|
||||
const main = response.main;
|
||||
const sys = response.sys;
|
||||
@@ -137,10 +141,11 @@ export async function getWeather(args?: Record<string, unknown>): Promise<any |
|
||||
windSpeed: wind.speed,
|
||||
},
|
||||
};
|
||||
} catch (e: any) {
|
||||
} catch (e: unknown) {
|
||||
logger.error("failed", {duration: logger.duration(startedAt), error: e});
|
||||
logError(e);
|
||||
return null;
|
||||
} finally {
|
||||
console.log("END: getWeather()");
|
||||
logger.debug("done", {duration: logger.duration(startedAt)});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
// Gemini provider runner extracted from unified-ai-runner.ts.
|
||||
import {getGeminiTools} from "./tool-mappers";
|
||||
import {TelegramStreamMessage} from "./telegram-stream-message";
|
||||
import {ToolRuntimeContext} from "./tools/runtime";
|
||||
import {GeminiMessage} from "./gemini-chat-message";
|
||||
import {createGoogleGenAiClient} from "./ai-runtime-target";
|
||||
import {aiLog, aiLogDuration, aiLogProviderTarget, aiLogToolCall} from "../logging/ai-logger";
|
||||
|
||||
import {AsyncIterableStream, GeminiFunctionCallLike, GeminiResponseLike, MAX_TOOL_ROUNDS, RuntimeConfigSnapshot, ToolCallData, ToolExecutionMemory, executeToolBatch, roundStatus, safeJsonParseObject, GeminiGenerationRequest} from "./unified-ai-runner.shared";
|
||||
|
||||
function collectGeminiResponseText(response: GeminiResponseLike & { text?: string }): string {
|
||||
if (typeof response.text === "string") return response.text;
|
||||
|
||||
return (response.candidates ?? [])
|
||||
.flatMap(candidate => candidate.content?.parts ?? [])
|
||||
.map(part => part.text ?? "")
|
||||
.join("");
|
||||
}
|
||||
|
||||
function collectGeminiFunctionCalls(response: GeminiResponseLike): ToolCallData[] {
|
||||
const calls = response.functionCalls
|
||||
?? (response.candidates ?? []).flatMap(candidate => {
|
||||
return (candidate.content?.parts ?? [])
|
||||
.map(part => part.functionCall)
|
||||
.filter((call): call is GeminiFunctionCallLike => !!call);
|
||||
});
|
||||
|
||||
return (calls ?? []).map((call, index) => ({
|
||||
id: call.id ?? `gemini_${index}_${call.name ?? "call"}`,
|
||||
name: call.name ?? "",
|
||||
argumentsText: JSON.stringify(call.args ?? {}),
|
||||
})).filter((call: ToolCallData) => call.name);
|
||||
}
|
||||
|
||||
function mergeGeminiFunctionCalls(existing: ToolCallData[], next: ToolCallData[]): ToolCallData[] {
|
||||
const merged = [...existing];
|
||||
for (const call of next) {
|
||||
const index = merged.findIndex(item => item.id === call.id);
|
||||
if (index === -1) {
|
||||
merged.push(call);
|
||||
} else {
|
||||
merged[index] = call;
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
function appendGeminiToolRound(messages: GeminiMessage[], calls: ToolCallData[], results: string[]): void {
|
||||
messages.push({
|
||||
role: "model",
|
||||
parts: calls.map(call => ({
|
||||
functionCall: {
|
||||
id: call.id,
|
||||
name: call.name,
|
||||
args: safeJsonParseObject(call.argumentsText),
|
||||
},
|
||||
})),
|
||||
});
|
||||
|
||||
messages.push({
|
||||
role: "user",
|
||||
parts: calls.map((call, index) => ({
|
||||
functionResponse: {
|
||||
id: call.id,
|
||||
name: call.name,
|
||||
response: {result: results[index] ?? ""},
|
||||
},
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
export async function runGemini(
|
||||
messages: GeminiMessage[],
|
||||
streamMessage: TelegramStreamMessage,
|
||||
signal: AbortSignal,
|
||||
stream: boolean,
|
||||
firstRoundStatus: string,
|
||||
config: RuntimeConfigSnapshot,
|
||||
toolContext: ToolRuntimeContext,
|
||||
): Promise<void> {
|
||||
const runnerStartedAt = Date.now();
|
||||
const geminiAi = createGoogleGenAiClient(config.geminiChatTarget);
|
||||
|
||||
aiLog("info", "gemini.run.start", {
|
||||
stream,
|
||||
target: aiLogProviderTarget(config.geminiChatTarget),
|
||||
inputMessages: messages.length,
|
||||
hasToolInputFiles: !!toolContext.pythonInputFiles?.length,
|
||||
});
|
||||
|
||||
const toolMemory: ToolExecutionMemory = new Map();
|
||||
|
||||
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
|
||||
const roundStartedAt = Date.now();
|
||||
aiLog("debug", "gemini.round.start", {round, messages: messages.length, stream});
|
||||
if (signal.aborted) throw new Error("Aborted");
|
||||
|
||||
streamMessage.setStatus(roundStatus(round, firstRoundStatus) ?? "");
|
||||
await streamMessage.flush();
|
||||
|
||||
const request: GeminiGenerationRequest = {
|
||||
model: config.geminiChatTarget.model,
|
||||
contents: messages,
|
||||
config: {
|
||||
tools: getGeminiTools(),
|
||||
temperature: messages.length <= 2 ? 0 : 0.6,
|
||||
abortSignal: signal,
|
||||
},
|
||||
};
|
||||
|
||||
if (!stream) {
|
||||
const response = await geminiAi.models.generateContent(request) as unknown as GeminiResponseLike & {
|
||||
text?: string
|
||||
};
|
||||
const text = collectGeminiResponseText(response);
|
||||
streamMessage.append(text);
|
||||
const calls = collectGeminiFunctionCalls(response);
|
||||
aiLog(calls.length ? "info" : "success", calls.length ? "gemini.tool_calls" : "gemini.run.done", {
|
||||
round,
|
||||
duration: calls.length ? aiLogDuration(roundStartedAt) : aiLogDuration(runnerStartedAt),
|
||||
textChars: text.length,
|
||||
calls: calls.map(aiLogToolCall),
|
||||
});
|
||||
if (!calls.length) return;
|
||||
|
||||
appendGeminiToolRound(messages, calls, await executeToolBatch(calls, streamMessage, toolContext, toolMemory));
|
||||
continue;
|
||||
}
|
||||
|
||||
const response = await geminiAi.models.generateContentStream(request) as unknown as AsyncIterableStream<GeminiResponseLike & {
|
||||
text?: string
|
||||
}>;
|
||||
aiLog("debug", "gemini.stream.open", {round});
|
||||
let calls: ToolCallData[] = [];
|
||||
const roundTextStart = streamMessage.getText().length;
|
||||
for await (const chunk of response) {
|
||||
if (signal.aborted) throw new Error("Aborted");
|
||||
streamMessage.append(collectGeminiResponseText(chunk));
|
||||
calls = mergeGeminiFunctionCalls(calls, collectGeminiFunctionCalls(chunk));
|
||||
}
|
||||
|
||||
aiLog(calls.length ? "info" : "success", calls.length ? "gemini.tool_calls" : "gemini.run.done", {
|
||||
round,
|
||||
duration: calls.length ? aiLogDuration(roundStartedAt) : aiLogDuration(runnerStartedAt),
|
||||
textChars: streamMessage.getText().slice(roundTextStart).length,
|
||||
calls: calls.map(aiLogToolCall),
|
||||
});
|
||||
if (!calls.length) return;
|
||||
appendGeminiToolRound(messages, calls, await executeToolBatch(calls, streamMessage, toolContext, toolMemory));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class GeminiProviderRunner {
|
||||
static run = runGemini;
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
// Mistral provider runner extracted from unified-ai-runner.ts.
|
||||
import {Environment} from "../common/environment";
|
||||
import {getMistralTools} from "./tool-mappers";
|
||||
import {TelegramStreamMessage} from "./telegram-stream-message";
|
||||
import {ToolRuntimeContext} from "./tools/runtime";
|
||||
import {MistralChatMessage} from "./mistral-chat-message";
|
||||
import {createMistralClient} from "./ai-runtime-target";
|
||||
import {aiLog, aiLogDuration, aiLogProviderTarget, aiLogToolCall} from "../logging/ai-logger";
|
||||
|
||||
import {MAX_TOOL_ROUNDS, MistralDeltaLike, MistralDocumentReference, RuntimeConfigSnapshot, StreamingToolCallAccumulator, ToolCallData, ToolExecutionMemory, contentFromMistralDelta, executeToolBatch, mistralToolCalls, normalizeMistralToolCalls, roundStatus} from "./unified-ai-runner.shared";
|
||||
|
||||
export async function runMistral(
|
||||
messages: MistralChatMessage[],
|
||||
documents: MistralDocumentReference[],
|
||||
streamMessage: TelegramStreamMessage,
|
||||
signal: AbortSignal,
|
||||
stream: boolean,
|
||||
firstRoundStatus: string,
|
||||
config: RuntimeConfigSnapshot,
|
||||
toolContext: ToolRuntimeContext,
|
||||
): Promise<void> {
|
||||
const runnerStartedAt = Date.now();
|
||||
const mistralAi = createMistralClient(config.mistralChatTarget);
|
||||
aiLog("info", "mistral.run.start", {
|
||||
stream,
|
||||
target: aiLogProviderTarget(config.mistralChatTarget),
|
||||
inputMessages: messages.length,
|
||||
documents: documents.length,
|
||||
hasToolInputFiles: !!toolContext.pythonInputFiles?.length,
|
||||
});
|
||||
|
||||
const toolMemory: ToolExecutionMemory = new Map();
|
||||
|
||||
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
|
||||
const roundStartedAt = Date.now();
|
||||
aiLog("debug", "mistral.round.start", {round, messages: messages.length, stream});
|
||||
if (signal.aborted) throw new Error("Aborted");
|
||||
|
||||
streamMessage.setStatus(roundStatus(round, firstRoundStatus) ?? "");
|
||||
await streamMessage.flush();
|
||||
|
||||
if (!stream) {
|
||||
const request = {
|
||||
model: config.mistralChatTarget.model,
|
||||
messages,
|
||||
tools: getMistralTools(),
|
||||
documents: documents
|
||||
} as unknown as Parameters<typeof mistralAi.chat.complete>[0];
|
||||
const response = await mistralAi.chat.complete(request, {signal});
|
||||
const msg = response.choices?.[0]?.message;
|
||||
const text = typeof msg?.content === "string" ? msg.content : JSON.stringify(msg?.content ?? "");
|
||||
streamMessage.append(text);
|
||||
const calls = normalizeMistralToolCalls(mistralToolCalls(msg));
|
||||
aiLog(calls.length ? "info" : "success", calls.length ? "mistral.tool_calls" : "mistral.run.done", {
|
||||
round,
|
||||
duration: calls.length ? aiLogDuration(roundStartedAt) : aiLogDuration(runnerStartedAt),
|
||||
textChars: text.length,
|
||||
calls: calls.map(aiLogToolCall),
|
||||
});
|
||||
if (!calls.length) return;
|
||||
messages.push({
|
||||
role: "assistant",
|
||||
content: text,
|
||||
toolCalls: calls.map(call => ({
|
||||
id: call.id,
|
||||
function: {name: call.name, arguments: call.argumentsText},
|
||||
})),
|
||||
});
|
||||
const toolResults = await executeToolBatch(calls, streamMessage, toolContext, toolMemory);
|
||||
for (const [index, call] of calls.entries()) {
|
||||
messages.push({
|
||||
role: "tool",
|
||||
name: call.name,
|
||||
toolCallId: call.id,
|
||||
content: toolResults[index] ?? "",
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const request = {
|
||||
model: config.mistralChatTarget.model,
|
||||
messages,
|
||||
tools: getMistralTools(),
|
||||
documents: documents
|
||||
} as unknown as Parameters<typeof mistralAi.chat.stream>[0];
|
||||
const streamResponse = await mistralAi.chat.stream(request, {signal});
|
||||
aiLog("debug", "mistral.stream.open", {round});
|
||||
let calls: ToolCallData[] = [];
|
||||
const roundTextStart = streamMessage.getText().length;
|
||||
const toolCallAccumulator = new StreamingToolCallAccumulator("mistral_stream", round);
|
||||
|
||||
for await (const event of streamResponse) {
|
||||
if (signal.aborted) throw new Error("Aborted");
|
||||
|
||||
const choice = event.data?.choices?.[0];
|
||||
const delta = choice?.delta;
|
||||
const mistralDelta = delta as MistralDeltaLike;
|
||||
|
||||
streamMessage.append(contentFromMistralDelta(mistralDelta));
|
||||
|
||||
const rawDeltaCalls = mistralToolCalls(mistralDelta);
|
||||
if (rawDeltaCalls.length) {
|
||||
calls = toolCallAccumulator.add(rawDeltaCalls);
|
||||
streamMessage.setStatus(Environment.getUseToolText(calls));
|
||||
await streamMessage.flush();
|
||||
}
|
||||
}
|
||||
aiLog(calls.length ? "info" : "success", calls.length ? "mistral.tool_calls" : "mistral.run.done", {
|
||||
round,
|
||||
duration: calls.length ? aiLogDuration(roundStartedAt) : aiLogDuration(runnerStartedAt),
|
||||
textChars: streamMessage.getText().slice(roundTextStart).length,
|
||||
calls: calls.map(aiLogToolCall),
|
||||
});
|
||||
if (!calls.length) return;
|
||||
const roundText = streamMessage.getText().slice(roundTextStart);
|
||||
messages.push({
|
||||
role: "assistant",
|
||||
content: roundText,
|
||||
toolCalls: calls.map(c => ({id: c.id, function: {name: c.name, arguments: c.argumentsText}}))
|
||||
});
|
||||
const toolResults = await executeToolBatch(calls, streamMessage, toolContext, toolMemory);
|
||||
for (const [index, call] of calls.entries()) {
|
||||
messages.push({
|
||||
role: "tool",
|
||||
name: call.name,
|
||||
toolCallId: call.id,
|
||||
content: toolResults[index] ?? "",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class MistralProviderRunner {
|
||||
static run = runMistral;
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
// Ollama provider runner extracted from unified-ai-runner.ts.
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import * as fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import {AiProvider} from "../model/ai-provider";
|
||||
import {Environment} from "../common/environment";
|
||||
import {bot, notesDir} from "../index";
|
||||
import {clamp, logError} from "../util/utils";
|
||||
import {getOllamaTools} from "./tool-mappers";
|
||||
import {TelegramStreamMessage} from "./telegram-stream-message";
|
||||
import {getModelCapabilities} from "./provider-model-runtime";
|
||||
import {ChatMessage} from "./chat-messages-types";
|
||||
import {ChatRequest, Tool} from "ollama";
|
||||
import {ToolRuntimeContext} from "./tools/runtime";
|
||||
import {enqueueTelegramApiCall} from "../util/telegram-api-queue";
|
||||
import {getCurrentDateTimeTool} from "./tools/datetime";
|
||||
import {getMarketRatesTool} from "./tools/market-rates";
|
||||
import {getWeatherTool} from "./tools/weather";
|
||||
import {loadOllamaModel, unloadAllOllamaModels} from "./tools/utils";
|
||||
import {createOllamaClient} from "./ai-runtime-target";
|
||||
import {GetNoteFileResult, GetNoteFileResultSchema} from "./tools/send-note-file";
|
||||
import {aiLog, aiLogDuration, aiLogMessageIdentity, aiLogProviderTarget, aiLogToolCall} from "../logging/ai-logger";
|
||||
|
||||
import {DEFAULT_OLLAMA_CONTEXT_SIZE, MAX_OLLAMA_CONTEXT_SIZE, MAX_TOOL_ROUNDS, MIN_OLLAMA_CONTEXT_SIZE, RuntimeConfigSnapshot, Think, ToolCallData, ToolExecutionMemory, allToolSchemaNames, appendOllamaToolResults, dedupeToolCalls, executeToolBatch, normalizeOllamaToolCalls, roundStatus, safeJsonParseObject, isRecord, isOllamaModelActive, OllamaToolCallLike} from "./unified-ai-runner.shared";
|
||||
import {latestUserTextFromOllamaMessages, looksLikeToolRankerJson, OllamaToolRanker} from "./unified-ai-runner.tool-ranker";
|
||||
|
||||
export async function runOllama(
|
||||
msg: Message,
|
||||
messages: ChatMessage[],
|
||||
streamMessage: TelegramStreamMessage,
|
||||
signal: AbortSignal,
|
||||
stream: boolean,
|
||||
think: Think,
|
||||
firstRoundStatus: string,
|
||||
config: RuntimeConfigSnapshot,
|
||||
toolContext: ToolRuntimeContext,
|
||||
contextSize?: number,
|
||||
): Promise<void> {
|
||||
const fromId = msg.from?.id;
|
||||
const runnerStartedAt = Date.now();
|
||||
|
||||
const audioCount = messages.reduce((sum, m) => sum + (m.audioParts?.length || m.audios?.length || 0), 0);
|
||||
const videoNoteCount = messages.reduce((sum, m) => sum + (m.videoNotes?.length ?? 0), 0);
|
||||
const imageCount = messages.reduce((sum, m) => sum + (m.imageParts?.length || m.images?.length || 0), 0);
|
||||
|
||||
const target = (audioCount || videoNoteCount) ? config.ollamaAudioTarget :
|
||||
imageCount ? config.ollamaVisionTarget :
|
||||
think ? config.ollamaThinkingTarget : config.ollamaChatTarget;
|
||||
const model = target.model;
|
||||
aiLog("info", "ollama.run.start", {
|
||||
stream,
|
||||
think,
|
||||
target: aiLogProviderTarget(target),
|
||||
requestedContextSize: contextSize,
|
||||
message: aiLogMessageIdentity(msg),
|
||||
counts: {messages: messages.length, images: imageCount, audio: audioCount, videoNotes: videoNoteCount},
|
||||
hasToolInputFiles: !!toolContext.pythonInputFiles?.length,
|
||||
});
|
||||
|
||||
const ollama = createOllamaClient(target);
|
||||
const modelInfo = await ollama.show({model});
|
||||
const modelInfoMap = isRecord(modelInfo.model_info) ? modelInfo.model_info : {};
|
||||
const contextKey = Object.keys(modelInfoMap).find(k => k.endsWith(".context_length"));
|
||||
// @ts-ignore
|
||||
const rawMaxContextLength = contextKey ? modelInfoMap[contextKey] : undefined;
|
||||
const parsedMaxContextLength =
|
||||
typeof rawMaxContextLength === "number"
|
||||
? rawMaxContextLength
|
||||
: typeof rawMaxContextLength === "string"
|
||||
? Number(rawMaxContextLength)
|
||||
: DEFAULT_OLLAMA_CONTEXT_SIZE;
|
||||
|
||||
const maxContextLength = Number.isFinite(parsedMaxContextLength)
|
||||
? parsedMaxContextLength
|
||||
: DEFAULT_OLLAMA_CONTEXT_SIZE;
|
||||
|
||||
const context = clamp(
|
||||
contextSize === -1 ? MAX_OLLAMA_CONTEXT_SIZE : contextSize ?? DEFAULT_OLLAMA_CONTEXT_SIZE,
|
||||
MIN_OLLAMA_CONTEXT_SIZE,
|
||||
maxContextLength ?? DEFAULT_OLLAMA_CONTEXT_SIZE
|
||||
);
|
||||
aiLog("debug", "ollama.context.resolved", {model, contextKey, maxContextLength, context});
|
||||
|
||||
const modelsToLoad = [model];
|
||||
|
||||
try {
|
||||
const activeModels = (await ollama.ps()).models.map(m => m.model);
|
||||
const oldSet = new Set(activeModels);
|
||||
const newSet = new Set(modelsToLoad);
|
||||
|
||||
const added = modelsToLoad.filter(m => !oldSet.has(m));
|
||||
const removed = activeModels.filter(m => !newSet.has(m));
|
||||
const diff = [...added, ...removed];
|
||||
aiLog("debug", "ollama.models.active", {activeModels, requiredModels: modelsToLoad, added, removed});
|
||||
if (diff.length) {
|
||||
aiLog("info", "ollama.models.unload_extra", {keep: modelsToLoad, diff});
|
||||
await unloadAllOllamaModels(ollama, modelsToLoad);
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
|
||||
if (!(await isOllamaModelActive(ollama, target))) {
|
||||
const loadStartedAt = Date.now();
|
||||
aiLog("info", "ollama.model.load.start", {model, context});
|
||||
const currentStatus = streamMessage.getStatus();
|
||||
streamMessage.setStatus(Environment.getLoadingModelText(model));
|
||||
await streamMessage.flush();
|
||||
if (await loadOllamaModel(model, ollama, context)) {
|
||||
aiLog("success", "ollama.model.load.done", {model, duration: aiLogDuration(loadStartedAt)});
|
||||
streamMessage.setStatus(currentStatus ?? Environment.waitThinkText);
|
||||
await streamMessage.flush();
|
||||
}
|
||||
} else {
|
||||
aiLog("debug", "ollama.model.already_loaded", {model});
|
||||
}
|
||||
|
||||
let interval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
if (!stream) {
|
||||
let typingInFlight = false;
|
||||
const applyTyping = async () => {
|
||||
if (typingInFlight) return;
|
||||
typingInFlight = true;
|
||||
try {
|
||||
await enqueueTelegramApiCall(
|
||||
() => bot.sendChatAction({chat_id: msg.chat.id, action: "typing"}),
|
||||
{method: "sendChatAction", chatId: msg.chat.id, chatType: msg.chat.type}
|
||||
).catch(logError);
|
||||
} finally {
|
||||
typingInFlight = false;
|
||||
}
|
||||
};
|
||||
|
||||
await applyTyping();
|
||||
interval = setInterval(() => {
|
||||
applyTyping().catch(logError);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
const toolMemory: ToolExecutionMemory = new Map();
|
||||
|
||||
try {
|
||||
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
|
||||
const roundStartedAt = Date.now();
|
||||
aiLog("debug", "ollama.round.start", {
|
||||
round,
|
||||
context,
|
||||
messages: messages.length,
|
||||
stream,
|
||||
think: audioCount ? false : think,
|
||||
});
|
||||
|
||||
const request: ChatRequest = {
|
||||
model: model,
|
||||
messages: messages,
|
||||
think: audioCount ? false : think,
|
||||
options: {
|
||||
temperature: 0.6,
|
||||
num_ctx: context,
|
||||
}
|
||||
};
|
||||
|
||||
let activeToolNames: string[] = [];
|
||||
if ((await getModelCapabilities(AiProvider.OLLAMA, model, "tools"))?.tools?.supported) {
|
||||
const availableOllamaTools: Tool[] = fromId !== Environment.CREATOR_ID
|
||||
? [getCurrentDateTimeTool, getMarketRatesTool, getWeatherTool]
|
||||
: getOllamaTools() as Tool[];
|
||||
|
||||
aiLog("debug", "ollama.tools.available", {
|
||||
round,
|
||||
tools: allToolSchemaNames(availableOllamaTools),
|
||||
rankerEnabled: !!config.ollamaToolRankerTarget,
|
||||
});
|
||||
|
||||
const rankerSelection = await new OllamaToolRanker(config).selectTools({
|
||||
userQuery: latestUserTextFromOllamaMessages(messages),
|
||||
availableTools: availableOllamaTools,
|
||||
round,
|
||||
signal,
|
||||
});
|
||||
|
||||
activeToolNames = rankerSelection.selectedNames;
|
||||
if (rankerSelection.tools.length > 0) {
|
||||
request.tools = rankerSelection.tools;
|
||||
} else {
|
||||
delete request.tools;
|
||||
}
|
||||
|
||||
aiLog("debug", "ollama.tools.selected", {
|
||||
round,
|
||||
tools: activeToolNames,
|
||||
count: activeToolNames.length,
|
||||
usedRanker: rankerSelection.usedRanker,
|
||||
missing: rankerSelection.missing,
|
||||
});
|
||||
}
|
||||
|
||||
if (!stream) {
|
||||
const response = await ollama.chat({
|
||||
...request,
|
||||
stream: false
|
||||
});
|
||||
|
||||
const message = response.message;
|
||||
const rawContent = message?.content ?? "";
|
||||
|
||||
const nativeCalls = dedupeToolCalls(
|
||||
normalizeOllamaToolCalls(
|
||||
message?.tool_calls as readonly OllamaToolCallLike[] | undefined,
|
||||
round,
|
||||
),
|
||||
);
|
||||
|
||||
const responseText = rawContent;
|
||||
|
||||
if (looksLikeToolRankerJson(responseText)) {
|
||||
aiLog("error", "ollama.response.looks_like_tool_ranker_json", {
|
||||
round,
|
||||
preview: responseText.slice(0, 800),
|
||||
target: aiLogProviderTarget(target),
|
||||
});
|
||||
throw new Error("Ollama chat model returned tool-ranker JSON. Check that OLLAMA chat target and OLLAMA tools/ranker target are not mixed up.");
|
||||
}
|
||||
|
||||
streamMessage.append(responseText);
|
||||
|
||||
aiLog("debug", "ollama.response.received", {
|
||||
round,
|
||||
duration: aiLogDuration(roundStartedAt),
|
||||
textChars: responseText.length,
|
||||
nativeToolCallCount: nativeCalls.length,
|
||||
});
|
||||
|
||||
if (!nativeCalls.length) {
|
||||
aiLog("success", "ollama.run.done", {round, duration: aiLogDuration(runnerStartedAt)});
|
||||
break;
|
||||
}
|
||||
|
||||
const calls = nativeCalls;
|
||||
|
||||
aiLog("info", "ollama.tool_calls", {
|
||||
round,
|
||||
calls: calls.map(aiLogToolCall),
|
||||
});
|
||||
|
||||
messages.push({
|
||||
role: "assistant",
|
||||
content: responseText,
|
||||
tool_calls: calls.map(c => ({
|
||||
function: {
|
||||
name: c.name,
|
||||
arguments: safeJsonParseObject(c.argumentsText),
|
||||
},
|
||||
})),
|
||||
});
|
||||
|
||||
appendOllamaToolResults(
|
||||
messages,
|
||||
calls,
|
||||
await executeToolBatch(calls, streamMessage, toolContext, toolMemory),
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const response = await ollama.chat({
|
||||
...request,
|
||||
stream: true
|
||||
});
|
||||
|
||||
aiLog("debug", "ollama.stream.open", {round});
|
||||
const calls: ToolCallData[] = [];
|
||||
const roundTextStart = streamMessage.getText().length;
|
||||
const abortOllamaResponse = () => response.abort?.();
|
||||
signal.addEventListener("abort", abortOllamaResponse, {once: true});
|
||||
if (signal.aborted) abortOllamaResponse();
|
||||
try {
|
||||
for await (const chunk of response) {
|
||||
const localToolCalls: ToolCallData[] = [];
|
||||
|
||||
localToolCalls.push(...normalizeOllamaToolCalls(
|
||||
chunk.message.tool_calls as readonly OllamaToolCallLike[] | undefined,
|
||||
round,
|
||||
));
|
||||
|
||||
const newStatus = roundStatus(round, firstRoundStatus, chunk.message.content, localToolCalls, !!chunk.message.thinking);
|
||||
const previousStatus = streamMessage.getStatus();
|
||||
if (newStatus && newStatus !== Environment.waitThinkText) {
|
||||
streamMessage.setStatus(newStatus);
|
||||
} else {
|
||||
streamMessage.clearStatus();
|
||||
}
|
||||
|
||||
if (streamMessage.getStatus() !== previousStatus && previousStatus && newStatus !== Environment.waitThinkText) {
|
||||
await streamMessage.flush();
|
||||
}
|
||||
|
||||
if (signal.aborted) {
|
||||
response.abort?.();
|
||||
throw new Error("Aborted");
|
||||
}
|
||||
|
||||
if (!(chunk.message?.thinking && streamMessage.getStatus() !== Environment.reasoningText)) {
|
||||
streamMessage.append(chunk.message?.content ?? "");
|
||||
}
|
||||
|
||||
calls.push(...normalizeOllamaToolCalls(
|
||||
chunk.message?.tool_calls as readonly OllamaToolCallLike[] | undefined,
|
||||
round,
|
||||
));
|
||||
|
||||
if (chunk.done) {
|
||||
aiLog("debug", "ollama.stream.done", {
|
||||
round,
|
||||
duration: aiLogDuration(roundStartedAt),
|
||||
textChars: streamMessage.getText().slice(roundTextStart).length,
|
||||
toolCallCount: calls.length,
|
||||
});
|
||||
await streamMessage.flush(streamMessage.regenerateKeyboard(), true);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
signal.removeEventListener("abort", abortOllamaResponse);
|
||||
}
|
||||
|
||||
const streamedRoundText = streamMessage.getText().slice(roundTextStart);
|
||||
if (!calls.length && looksLikeToolRankerJson(streamedRoundText)) {
|
||||
streamMessage.replaceText(streamMessage.getText().slice(0, roundTextStart));
|
||||
aiLog("error", "ollama.response.looks_like_tool_ranker_json", {
|
||||
round,
|
||||
preview: streamedRoundText.slice(0, 800),
|
||||
target: aiLogProviderTarget(target),
|
||||
});
|
||||
throw new Error("Ollama chat model returned tool-ranker JSON. Check that OLLAMA chat target and OLLAMA tools/ranker target are not mixed up.");
|
||||
}
|
||||
|
||||
if (!calls.length) {
|
||||
aiLog("success", "ollama.run.done", {
|
||||
round,
|
||||
duration: aiLogDuration(runnerStartedAt),
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
calls.splice(0, calls.length, ...dedupeToolCalls(calls));
|
||||
|
||||
aiLog("info", "ollama.tool_calls", {
|
||||
round,
|
||||
calls: calls.map(aiLogToolCall),
|
||||
});
|
||||
|
||||
const roundText = streamMessage.getText().slice(roundTextStart);
|
||||
|
||||
messages.push({
|
||||
role: "assistant",
|
||||
content: roundText,
|
||||
tool_calls: calls.map(c => ({
|
||||
function: {
|
||||
name: c.name,
|
||||
arguments: safeJsonParseObject(c.argumentsText),
|
||||
},
|
||||
})),
|
||||
});
|
||||
|
||||
const toolResults = await executeToolBatch(calls, streamMessage, toolContext, toolMemory);
|
||||
|
||||
let successGetNoteFileResult: GetNoteFileResult | undefined = undefined;
|
||||
|
||||
for (const toolResult of toolResults) {
|
||||
try {
|
||||
const raw = JSON.parse(toolResult);
|
||||
const res = GetNoteFileResultSchema.safeParse(raw);
|
||||
|
||||
if (res.success && res.data.success) {
|
||||
successGetNoteFileResult = res.data;
|
||||
}
|
||||
} catch {
|
||||
// Not every tool result is JSON.
|
||||
}
|
||||
}
|
||||
|
||||
if (successGetNoteFileResult && "attachment" in successGetNoteFileResult) {
|
||||
await bot.sendDocument({
|
||||
chat_id: msg.chat.id,
|
||||
reply_parameters: {
|
||||
message_id: msg.message_id,
|
||||
},
|
||||
document: fs.createReadStream(path.join(notesDir, successGetNoteFileResult.attachment.relativePath)),
|
||||
}).catch(logError);
|
||||
}
|
||||
|
||||
appendOllamaToolResults(messages, calls, toolResults);
|
||||
}
|
||||
} finally {
|
||||
if (interval) clearInterval(interval);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class OllamaProviderRunner {
|
||||
static run = runOllama;
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
// OpenAI and OpenAI-compatible provider runners extracted from unified-ai-runner.ts.
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {Environment} from "../common/environment";
|
||||
import {getOpenAITools} from "./tool-mappers";
|
||||
import {TelegramStreamMessage} from "./telegram-stream-message";
|
||||
import {ToolRuntimeContext} from "./tools/runtime";
|
||||
import {OpenAIChatMessage} from "./openai-chat-message";
|
||||
import type {ResponseCreateParamsNonStreaming, ResponseCreateParamsStreaming, ResponseInputItem, ResponseStreamEvent} from "openai/resources/responses/responses";
|
||||
import type {ChatCompletionCreateParamsNonStreaming, ChatCompletionCreateParamsStreaming} from "openai/resources/chat/completions";
|
||||
import {createGeminiOpenAiClient, createOpenAiClient} from "./ai-runtime-target";
|
||||
import {aiLog, aiLogDuration, aiLogMessageIdentity, aiLogProviderTarget, aiLogToolCall} from "../logging/ai-logger";
|
||||
|
||||
import {AsyncIterableStream, MAX_TOOL_ROUNDS, OPENAI_IMAGE_PARTIALS, OpenAiChatCompletionResponseLike, OpenAiChatToolCallLike, OpenAiCompatibleChatMessage, OpenAiCompatibleContentPart, OpenAiResponseLike, OpenAiResponseOutputItem, RuntimeConfigSnapshot, ToolCallData, StreamingToolCallAccumulator, collectOpenAiResponseFunctionCalls, collectOpenAiResponseImages, collectOpenAiResponseText, executeToolBatch, getOpenAIResponsesToolsWithImage, openAiResponseItemCallId, safeJsonParseObject, showOpenAiGeneratedImage, ToolExecutionMemory, isRecord, roundStatus, OpenAiChatCompletionStreamChunkLike} from "./unified-ai-runner.shared";
|
||||
|
||||
export async function runOpenAi(
|
||||
messages: OpenAIChatMessage[],
|
||||
streamMessage: TelegramStreamMessage,
|
||||
signal: AbortSignal,
|
||||
stream: boolean,
|
||||
firstRoundStatus: string,
|
||||
sourceMessage: Message,
|
||||
config: RuntimeConfigSnapshot,
|
||||
toolContext: ToolRuntimeContext,
|
||||
): Promise<void> {
|
||||
// TODO: 13.05.2026: remove
|
||||
firstRoundStatus;
|
||||
const runnerStartedAt = Date.now();
|
||||
let responseInput: unknown[] = [...messages];
|
||||
const openAi = createOpenAiClient(config.openAiChatTarget);
|
||||
|
||||
aiLog("info", "openai.run.start", {
|
||||
stream,
|
||||
target: aiLogProviderTarget(config.openAiChatTarget),
|
||||
imageTarget: aiLogProviderTarget(config.openAiImageTarget),
|
||||
inputMessages: messages.length,
|
||||
sourceMessage: aiLogMessageIdentity(sourceMessage),
|
||||
hasToolInputFiles: !!toolContext.pythonInputFiles?.length,
|
||||
});
|
||||
|
||||
const toolMemory: ToolExecutionMemory = new Map();
|
||||
|
||||
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
|
||||
const roundStartedAt = Date.now();
|
||||
aiLog("debug", "openai.round.start", {round, inputItems: responseInput.length, stream});
|
||||
|
||||
if (!stream) {
|
||||
const request: ResponseCreateParamsNonStreaming = {
|
||||
model: config.openAiChatTarget.model,
|
||||
input: responseInput as ResponseInputItem[],
|
||||
tools: getOpenAIResponsesToolsWithImage(config) as ResponseCreateParamsNonStreaming["tools"],
|
||||
instructions: config.systemPrompt,
|
||||
};
|
||||
const response = await openAi.responses.create(request, {signal}) as unknown as OpenAiResponseLike;
|
||||
|
||||
const responseText = collectOpenAiResponseText(response);
|
||||
streamMessage.append(responseText);
|
||||
aiLog("debug", "openai.response.received", {
|
||||
round,
|
||||
duration: aiLogDuration(roundStartedAt),
|
||||
textChars: responseText.length,
|
||||
outputItems: response?.output?.length ?? 0,
|
||||
});
|
||||
const images = collectOpenAiResponseImages(response);
|
||||
if (images.length) {
|
||||
await showOpenAiGeneratedImage(
|
||||
streamMessage,
|
||||
sourceMessage,
|
||||
images[images.length - 1],
|
||||
`final_${round}`,
|
||||
Environment.getImageGenDoneText(config.openAiImageTarget.model),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
const calls = collectOpenAiResponseFunctionCalls(response);
|
||||
aiLog(calls.length ? "info" : "success", calls.length ? "openai.tool_calls" : "openai.run.done", {
|
||||
round,
|
||||
duration: calls.length ? aiLogDuration(roundStartedAt) : aiLogDuration(runnerStartedAt),
|
||||
calls: calls.map(call => ({
|
||||
id: call.callId,
|
||||
name: call.name,
|
||||
arguments: safeJsonParseObject(call.argumentsText)
|
||||
})),
|
||||
});
|
||||
if (!calls.length) return;
|
||||
|
||||
const toolCalls = calls.map(call => ({
|
||||
id: call.callId,
|
||||
name: call.name,
|
||||
argumentsText: call.argumentsText,
|
||||
}));
|
||||
const toolResults = await executeToolBatch(toolCalls, streamMessage, toolContext, toolMemory);
|
||||
const toolOutputs = calls.map((call, index) => ({
|
||||
type: "function_call_output" as const,
|
||||
call_id: call.callId,
|
||||
output: toolResults[index] ?? "",
|
||||
}));
|
||||
responseInput = [...responseInput, ...(response.output ?? []), ...toolOutputs];
|
||||
continue;
|
||||
}
|
||||
|
||||
let completedResponse: OpenAiResponseLike | null = null;
|
||||
const request: ResponseCreateParamsStreaming = {
|
||||
model: config.openAiChatTarget.model,
|
||||
input: responseInput as ResponseInputItem[],
|
||||
stream: true,
|
||||
tools: getOpenAIResponsesToolsWithImage(config) as ResponseCreateParamsStreaming["tools"],
|
||||
};
|
||||
const response = await openAi.responses.create(request, {signal}) as unknown as AsyncIterableStream<ResponseStreamEvent>;
|
||||
|
||||
aiLog("debug", "openai.stream.open", {round});
|
||||
|
||||
let localToolCalls: ToolCallData[] = [];
|
||||
for await (const event of response) {
|
||||
if (signal.aborted) throw new Error("Aborted");
|
||||
|
||||
switch (event.type) {
|
||||
case "response.output_text.delta":
|
||||
streamMessage.append(event.delta ?? "");
|
||||
break;
|
||||
case "response.image_generation_call.in_progress":
|
||||
streamMessage.setStatus(Environment.startingImageGenText);
|
||||
await streamMessage.flush();
|
||||
break;
|
||||
case "response.image_generation_call.generating":
|
||||
streamMessage.setStatus(Environment.imageGenText);
|
||||
await streamMessage.flush();
|
||||
break;
|
||||
case "response.image_generation_call.partial_image": {
|
||||
const iteration = (event.partial_image_index ?? 0) + 1;
|
||||
await showOpenAiGeneratedImage(
|
||||
streamMessage,
|
||||
sourceMessage,
|
||||
event.partial_image_b64,
|
||||
`partial_${round}_${iteration}`,
|
||||
Environment.getPartialImageGenText(iteration, OPENAI_IMAGE_PARTIALS),
|
||||
false,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "response.image_generation_call.completed":
|
||||
streamMessage.setStatus(Environment.finalizingImageGenText);
|
||||
await streamMessage.flush();
|
||||
break;
|
||||
case "response.output_item.added":
|
||||
if (event.item.type === "function_call" && event.item.name) {
|
||||
const item = event.item as OpenAiResponseOutputItem & { id?: string };
|
||||
localToolCalls.push({
|
||||
id: openAiResponseItemCallId(item),
|
||||
name: item.name ?? "",
|
||||
argumentsText: item.arguments ?? "{}",
|
||||
});
|
||||
|
||||
aiLog("info", "openai.stream.tool_call.added", {
|
||||
round,
|
||||
toolCalls: localToolCalls.map(aiLogToolCall)
|
||||
});
|
||||
streamMessage.setStatus(Environment.getUseToolText(localToolCalls));
|
||||
await streamMessage.flush();
|
||||
}
|
||||
break;
|
||||
case "response.output_item.done":
|
||||
if (event.item.type === "function_call" && event.item.name) {
|
||||
const item = event.item as OpenAiResponseOutputItem & { id?: string };
|
||||
const itemId = openAiResponseItemCallId(item);
|
||||
const index = localToolCalls.findIndex(c => c.id === itemId);
|
||||
if (index !== -1) {
|
||||
localToolCalls.splice(index, 1);
|
||||
if (localToolCalls.length === 0) {
|
||||
streamMessage.clearStatus();
|
||||
} else {
|
||||
streamMessage.setStatus(Environment.getUseToolText(localToolCalls));
|
||||
}
|
||||
await streamMessage.flush();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "response.function_call_arguments.delta":
|
||||
break;
|
||||
case "response.function_call_arguments.done":
|
||||
break;
|
||||
|
||||
case "response.completed":
|
||||
completedResponse = event.response as unknown as OpenAiResponseLike;
|
||||
break;
|
||||
case "response.failed":
|
||||
throw new Error(event.response?.error?.message ?? "OpenAI response failed");
|
||||
case "error":
|
||||
throw new Error(event.message ?? event?.message ?? "OpenAI stream error");
|
||||
}
|
||||
}
|
||||
|
||||
if (!completedResponse) throw new Error("OpenAI did not return the final response.completed event.");
|
||||
|
||||
aiLog("debug", "openai.stream.completed", {
|
||||
round,
|
||||
duration: aiLogDuration(roundStartedAt),
|
||||
outputItems: completedResponse?.output?.length ?? 0,
|
||||
});
|
||||
|
||||
const images = collectOpenAiResponseImages(completedResponse);
|
||||
if (images.length) {
|
||||
await showOpenAiGeneratedImage(
|
||||
streamMessage,
|
||||
sourceMessage,
|
||||
images[images.length - 1],
|
||||
`final_${round}`,
|
||||
Environment.getImageGenDoneText(config.openAiImageTarget.model),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
const calls = collectOpenAiResponseFunctionCalls(completedResponse);
|
||||
aiLog(calls.length ? "info" : "success", calls.length ? "openai.tool_calls" : "openai.run.done", {
|
||||
round,
|
||||
duration: calls.length ? aiLogDuration(roundStartedAt) : aiLogDuration(runnerStartedAt),
|
||||
calls: calls.map(call => ({
|
||||
id: call.callId,
|
||||
name: call.name,
|
||||
arguments: safeJsonParseObject(call.argumentsText)
|
||||
})),
|
||||
});
|
||||
if (!calls.length) return;
|
||||
|
||||
const toolCalls = calls.map(call => ({
|
||||
id: call.callId,
|
||||
name: call.name,
|
||||
argumentsText: call.argumentsText,
|
||||
}));
|
||||
const toolResults = await executeToolBatch(toolCalls, streamMessage, toolContext, toolMemory);
|
||||
const toolOutputs = calls.map((call, index) => ({
|
||||
type: "function_call_output",
|
||||
call_id: call.callId,
|
||||
output: toolResults[index] ?? "",
|
||||
}));
|
||||
responseInput = [...responseInput, ...(completedResponse.output ?? []), ...toolOutputs];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function openAiResponseContentToText(content: unknown): string {
|
||||
if (typeof content === "string") return content;
|
||||
if (!Array.isArray(content)) return "";
|
||||
return content.map(part => isRecord(part) ? part.text ?? part.content ?? part.refusal ?? "" : "").join("");
|
||||
}
|
||||
|
||||
function openAiResponseMessagesToChatCompletions(messages: OpenAIChatMessage[]): OpenAiCompatibleChatMessage[] {
|
||||
return messages.map((message): OpenAiCompatibleChatMessage => {
|
||||
if (message.role === "system" || message.role === "assistant") {
|
||||
return {
|
||||
role: message.role,
|
||||
content: openAiResponseContentToText(message.content),
|
||||
};
|
||||
}
|
||||
|
||||
const content = Array.isArray(message.content)
|
||||
? message.content.map((part): OpenAiCompatibleContentPart => {
|
||||
if (isRecord(part) && part.type === "input_image") {
|
||||
return {
|
||||
type: "image_url",
|
||||
image_url: {url: String(part.image_url ?? "")},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: "text",
|
||||
text: isRecord(part) && typeof part.text === "string" ? part.text : "",
|
||||
};
|
||||
})
|
||||
: message.content;
|
||||
|
||||
return {role: "user", content};
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeOpenAiChatToolCalls(toolCalls: OpenAiChatToolCallLike[] = []): ToolCallData[] {
|
||||
return toolCalls.map((call, i) => ({
|
||||
id: call.id || `openai_chat_${Date.now()}_${i}`,
|
||||
name: call.function?.name || call.name || "",
|
||||
argumentsText: typeof call.function?.arguments === "string"
|
||||
? call.function.arguments
|
||||
: JSON.stringify(call.function?.arguments ?? call.arguments ?? {}),
|
||||
})).filter(call => call.name);
|
||||
}
|
||||
|
||||
async function appendOpenAiChatToolResults(
|
||||
messages: OpenAiCompatibleChatMessage[],
|
||||
calls: ToolCallData[],
|
||||
results: string[],
|
||||
): Promise<void> {
|
||||
for (const [index, call] of calls.entries()) {
|
||||
messages.push({
|
||||
role: "tool",
|
||||
tool_call_id: call.id,
|
||||
content: results[index] ?? "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function runOpenAiCompatibleChat(
|
||||
messages: OpenAIChatMessage[],
|
||||
streamMessage: TelegramStreamMessage,
|
||||
signal: AbortSignal,
|
||||
stream: boolean,
|
||||
firstRoundStatus: string,
|
||||
config: RuntimeConfigSnapshot,
|
||||
toolContext: ToolRuntimeContext,
|
||||
): Promise<void> {
|
||||
const runnerStartedAt = Date.now();
|
||||
const geminiOpenAi = createGeminiOpenAiClient(config.geminiChatTarget);
|
||||
const chatMessages = openAiResponseMessagesToChatCompletions(messages);
|
||||
const toolMemory: ToolExecutionMemory = new Map();
|
||||
|
||||
aiLog("info", "openai_compatible.run.start", {
|
||||
stream,
|
||||
target: aiLogProviderTarget(config.geminiChatTarget),
|
||||
inputMessages: messages.length,
|
||||
chatMessages: chatMessages.length,
|
||||
hasToolInputFiles: !!toolContext.pythonInputFiles?.length,
|
||||
});
|
||||
|
||||
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
|
||||
const roundStartedAt = Date.now();
|
||||
aiLog("debug", "openai_compatible.round.start", {round, messages: chatMessages.length, stream});
|
||||
streamMessage.setStatus(roundStatus(round, firstRoundStatus) ?? "");
|
||||
await streamMessage.flush();
|
||||
|
||||
if (!stream) {
|
||||
const request: ChatCompletionCreateParamsNonStreaming = {
|
||||
model: config.geminiChatTarget.model,
|
||||
messages: chatMessages,
|
||||
tools: getOpenAITools(),
|
||||
temperature: 0.6,
|
||||
};
|
||||
const response = await geminiOpenAi.chat.completions.create(request, {signal}) as unknown as OpenAiChatCompletionResponseLike;
|
||||
const message = response.choices?.[0]?.message;
|
||||
streamMessage.append(message?.content ?? "");
|
||||
const calls = normalizeOpenAiChatToolCalls(message?.tool_calls ?? []);
|
||||
aiLog(calls.length ? "info" : "success", calls.length ? "openai_compatible.tool_calls" : "openai_compatible.run.done", {
|
||||
round,
|
||||
duration: calls.length ? aiLogDuration(roundStartedAt) : aiLogDuration(runnerStartedAt),
|
||||
textChars: message?.content?.length ?? 0,
|
||||
calls: calls.map(aiLogToolCall),
|
||||
});
|
||||
if (!calls.length) return;
|
||||
|
||||
chatMessages.push({
|
||||
role: "assistant",
|
||||
content: message?.content ?? "",
|
||||
tool_calls: calls.map(call => ({
|
||||
id: call.id,
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: call.name,
|
||||
arguments: call.argumentsText,
|
||||
},
|
||||
})),
|
||||
});
|
||||
await appendOpenAiChatToolResults(chatMessages, calls, await executeToolBatch(calls, streamMessage, toolContext, toolMemory));
|
||||
continue;
|
||||
}
|
||||
|
||||
const request: ChatCompletionCreateParamsStreaming = {
|
||||
model: config.geminiChatTarget.model,
|
||||
messages: chatMessages,
|
||||
tools: getOpenAITools(),
|
||||
temperature: 0.6,
|
||||
stream: true,
|
||||
};
|
||||
const response = await geminiOpenAi.chat.completions.create(request, {signal}) as unknown as AsyncIterableStream<OpenAiChatCompletionStreamChunkLike>;
|
||||
|
||||
aiLog("debug", "openai_compatible.stream.open", {round});
|
||||
// const streamToolCalls: OpenAiChatToolCallLike[] = [];
|
||||
const roundTextStart = streamMessage.getText().length;
|
||||
const toolCallAccumulator = new StreamingToolCallAccumulator("openai_chat_stream", round);
|
||||
let calls: ToolCallData[] = [];
|
||||
|
||||
for await (const chunk of response) {
|
||||
if (signal.aborted) throw new Error("Aborted");
|
||||
|
||||
const delta = chunk.choices?.[0]?.delta;
|
||||
streamMessage.append(delta?.content ?? "");
|
||||
|
||||
if (delta?.tool_calls?.length) {
|
||||
calls = toolCallAccumulator.add(delta.tool_calls);
|
||||
streamMessage.setStatus(Environment.getUseToolText(calls));
|
||||
await streamMessage.flush();
|
||||
}
|
||||
}
|
||||
|
||||
// const calls = collectOpenAiChatStreamToolCalls(streamToolCalls);
|
||||
aiLog(calls.length ? "info" : "success", calls.length ? "openai_compatible.tool_calls" : "openai_compatible.run.done", {
|
||||
round,
|
||||
duration: calls.length ? aiLogDuration(roundStartedAt) : aiLogDuration(runnerStartedAt),
|
||||
textChars: streamMessage.getText().slice(roundTextStart).length,
|
||||
calls: calls.map(aiLogToolCall),
|
||||
});
|
||||
if (!calls.length) return;
|
||||
|
||||
const roundText = streamMessage.getText().slice(roundTextStart);
|
||||
chatMessages.push({
|
||||
role: "assistant",
|
||||
content: roundText,
|
||||
tool_calls: calls.map(call => ({
|
||||
id: call.id,
|
||||
type: "function",
|
||||
function: {
|
||||
name: call.name,
|
||||
arguments: call.argumentsText,
|
||||
},
|
||||
})),
|
||||
});
|
||||
await appendOpenAiChatToolResults(chatMessages, calls, await executeToolBatch(calls, streamMessage, toolContext, toolMemory));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class OpenAiProviderRunner {
|
||||
static run = runOpenAi;
|
||||
}
|
||||
|
||||
export class OpenAiCompatibleProviderRunner {
|
||||
static run = runOpenAiCompatibleChat;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,225 @@
|
||||
import {Tool} from "ollama";
|
||||
import {AiRuntimeTarget, createOllamaClient} from "./ai-runtime-target";
|
||||
import {aiLog, aiLogDuration, aiLogProviderTarget} from "../logging/ai-logger";
|
||||
import {allToolSchemaNames, isRecord, JsonObject, RuntimeConfigSnapshot, safeJsonParseObject, toolSchemaNames} from "./unified-ai-runner.shared";
|
||||
|
||||
type RankedToolStep = {
|
||||
t: string | string[];
|
||||
h?: string;
|
||||
from?: string;
|
||||
};
|
||||
|
||||
type RankedToolPlan = {
|
||||
s?: RankedToolStep[];
|
||||
m?: string;
|
||||
};
|
||||
|
||||
export type ToolRankerSelection = {
|
||||
tools: Tool[];
|
||||
selectedNames: string[];
|
||||
missing: string;
|
||||
raw: string;
|
||||
usedRanker: boolean;
|
||||
};
|
||||
|
||||
export class OllamaToolRanker {
|
||||
constructor(private readonly config: RuntimeConfigSnapshot) {}
|
||||
|
||||
async selectTools(args: {
|
||||
userQuery: string;
|
||||
availableTools: Tool[];
|
||||
round: number;
|
||||
signal: AbortSignal;
|
||||
}): Promise<ToolRankerSelection> {
|
||||
const {availableTools, round, signal, userQuery} = args;
|
||||
const target = this.config.ollamaToolRankerTarget;
|
||||
|
||||
if (!availableTools.length) {
|
||||
return {tools: [], selectedNames: [], missing: "", raw: "", usedRanker: false};
|
||||
}
|
||||
|
||||
// Ranker disabled/unconfigured: keep old behavior and expose all allowed tools.
|
||||
if (!target?.model) {
|
||||
return {
|
||||
tools: availableTools,
|
||||
selectedNames: allToolSchemaNames(availableTools),
|
||||
missing: "",
|
||||
raw: "",
|
||||
usedRanker: false,
|
||||
};
|
||||
}
|
||||
|
||||
const startedAt = Date.now();
|
||||
const availableNames = new Set(allToolSchemaNames(availableTools));
|
||||
const prompt = this.config.rankerToolPrompt?.trim() || DEFAULT_TOOL_RANKER_PROMPT;
|
||||
const toolsForPrompt = availableTools.map(tool => ({
|
||||
names: toolSchemaNames(tool),
|
||||
schema: tool,
|
||||
}));
|
||||
|
||||
aiLog("debug", "ollama.tool_ranker.start", {
|
||||
round,
|
||||
target: aiLogProviderTarget(target),
|
||||
queryChars: userQuery.length,
|
||||
availableTools: [...availableNames],
|
||||
});
|
||||
|
||||
try {
|
||||
const ollama = createOllamaClient(target as AiRuntimeTarget);
|
||||
const response = await ollama.chat({
|
||||
model: target.model,
|
||||
messages: [
|
||||
{role: "system", content: prompt},
|
||||
{
|
||||
role: "user",
|
||||
content: JSON.stringify({
|
||||
q: userQuery,
|
||||
tools: toolsForPrompt,
|
||||
}),
|
||||
},
|
||||
],
|
||||
stream: false,
|
||||
options: {
|
||||
temperature: 0,
|
||||
num_ctx: 8192,
|
||||
},
|
||||
});
|
||||
|
||||
if (signal.aborted) throw new Error("Aborted");
|
||||
|
||||
const raw = response.message?.content?.trim() ?? "";
|
||||
const plan = parseToolRankerPlan(raw);
|
||||
const selectedNames = normalizeToolRankerNames(plan, availableNames);
|
||||
const selectedNameSet = new Set(selectedNames);
|
||||
const tools = availableTools.filter(tool => toolSchemaNames(tool).some(name => selectedNameSet.has(name)));
|
||||
const missing = typeof plan?.m === "string" ? plan.m.trim() : "";
|
||||
|
||||
aiLog("debug", "ollama.tool_ranker.done", {
|
||||
round,
|
||||
duration: aiLogDuration(startedAt),
|
||||
selectedNames,
|
||||
selectedCount: tools.length,
|
||||
missing,
|
||||
rawPreview: raw.slice(0, 800),
|
||||
});
|
||||
|
||||
// Important: if the plan is valid and empty, use NO tools. Do not fall back to all tools.
|
||||
return {tools, selectedNames, missing, raw, usedRanker: true};
|
||||
} catch (error) {
|
||||
if (String(error).includes("Aborted")) throw error;
|
||||
|
||||
aiLog("warn", "ollama.tool_ranker.failed.fallback_all_allowed", {
|
||||
round,
|
||||
target: aiLogProviderTarget(target),
|
||||
duration: aiLogDuration(startedAt),
|
||||
error,
|
||||
});
|
||||
|
||||
// Ranker transport/model failure is different from "ranker returned empty plan".
|
||||
// In that case, preserve availability rather than silently disabling tools.
|
||||
return {
|
||||
tools: availableTools,
|
||||
selectedNames: allToolSchemaNames(availableTools),
|
||||
missing: "",
|
||||
raw: "",
|
||||
usedRanker: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function latestUserTextFromOllamaMessages(messages: readonly { role?: string; content?: unknown }[]): string {
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const message = messages[i];
|
||||
if (message?.role !== "user") continue;
|
||||
if (typeof message.content === "string") return message.content;
|
||||
if (Array.isArray(message.content)) {
|
||||
return message.content
|
||||
.map(part => isRecord(part) && typeof part.text === "string" ? part.text : "")
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export function looksLikeToolRankerJson(text: string): boolean {
|
||||
const parsed = safeJsonParseObject(extractJsonObjectText(text) ?? text);
|
||||
return Array.isArray(parsed.s) && typeof parsed.m === "string";
|
||||
}
|
||||
|
||||
function parseToolRankerPlan(raw: string): RankedToolPlan | undefined {
|
||||
const jsonText = extractJsonObjectText(raw);
|
||||
if (!jsonText) return undefined;
|
||||
|
||||
const parsed = safeJsonParseObject(jsonText) as JsonObject;
|
||||
if (!Array.isArray(parsed.s)) return undefined;
|
||||
|
||||
return parsed as RankedToolPlan;
|
||||
}
|
||||
|
||||
function normalizeToolRankerNames(plan: RankedToolPlan | undefined, availableNames: Set<string>): string[] {
|
||||
if (!plan?.s?.length) return [];
|
||||
|
||||
const result: string[] = [];
|
||||
for (const step of plan.s) {
|
||||
const rawNames = Array.isArray(step.t) ? step.t : [step.t];
|
||||
for (const rawName of rawNames) {
|
||||
if (typeof rawName !== "string") continue;
|
||||
const name = rawName.trim();
|
||||
if (availableNames.has(name) && !result.includes(name)) {
|
||||
result.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function extractJsonObjectText(raw: string): string | undefined {
|
||||
const text = raw.trim()
|
||||
.replace(/^```(?:json)?\s*/i, "")
|
||||
.replace(/\s*```$/i, "")
|
||||
.trim();
|
||||
|
||||
const start = text.indexOf("{");
|
||||
if (start === -1) return undefined;
|
||||
|
||||
let depth = 0;
|
||||
let inString = false;
|
||||
let escaped = false;
|
||||
|
||||
for (let i = start; i < text.length; i++) {
|
||||
const ch = text[i];
|
||||
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "\\") {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '"') {
|
||||
inString = !inString;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inString) continue;
|
||||
|
||||
if (ch === "{") depth++;
|
||||
if (ch === "}") depth--;
|
||||
|
||||
if (depth === 0) {
|
||||
return text.slice(start, i + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const DEFAULT_TOOL_RANKER_PROMPT = `You are a tool router. Return strict compact JSON only.
|
||||
Schema: {"s":[{"t":"tool_name","h":"short input hint","from":"previous_tool.output_or_empty"}],"m":""}
|
||||
Use tools only when they are needed. If no tool is needed, return {"s":[],"m":""}.
|
||||
Never answer the user. Never explain. Never use markdown.`;
|
||||
+130
-1946
File diff suppressed because it is too large
Load Diff
@@ -96,8 +96,8 @@ export class AiCancel extends CallbackCommand {
|
||||
{method: "editMessageText", chatId: message.chat.id, chatType: message.chat.type}
|
||||
);
|
||||
|
||||
if (result && result !== true) {
|
||||
await MessageStore.put({...result, text: cancelledText} as Message);
|
||||
if (result) {
|
||||
await MessageStore.put({...(result as object), text: cancelledText} as Message);
|
||||
} else {
|
||||
await MessageStore.put({
|
||||
chatId: message.chat.id,
|
||||
|
||||
@@ -7,7 +7,7 @@ import {StoredMessage} from "../model/stored-message";
|
||||
import {cutPrefixes, logError} from "../util/utils";
|
||||
import {runUnifiedAi} from "../ai/unified-ai-runner";
|
||||
import {AI_REGENERATE_CALLBACK, parseAiRegenerateCallbackData} from "../ai/regenerate-callback";
|
||||
import {isAiProviderConfigured, resolveEffectiveAiProviderForUser} from "../common/user-ai-settings";
|
||||
import {resolveEffectiveAiProviderForUser} from "../common/user-ai-settings";
|
||||
import {Environment} from "../common/environment";
|
||||
|
||||
export class AiRegenerate extends CallbackCommand {
|
||||
@@ -28,9 +28,11 @@ export class AiRegenerate extends CallbackCommand {
|
||||
const sourceFromId = source.stored?.fromId ?? source.message.from?.id;
|
||||
if (!sourceFromId || (sourceFromId !== query.from.id && query.from.id !== Environment.CREATOR_ID)) return;
|
||||
|
||||
const provider = isAiProviderConfigured(parsed.provider)
|
||||
? parsed.provider
|
||||
: await resolveEffectiveAiProviderForUser(source.message.from?.id ?? query.from.id);
|
||||
const provider =
|
||||
// isAiProviderConfigured(parsed.provider)
|
||||
// ? parsed.provider
|
||||
// :
|
||||
await resolveEffectiveAiProviderForUser(source.message.from?.id ?? query.from.id);
|
||||
const text = cutPrefixes(source.stored ?? source.message) ?? "";
|
||||
|
||||
runUnifiedAi({
|
||||
|
||||
+8
-6
@@ -21,8 +21,9 @@ export class Ae extends Command {
|
||||
try {
|
||||
let result = this.executeEvaluation(match);
|
||||
await oldSendMessage(msg, result).catch(async () => await errorPlaceholder(msg));
|
||||
} catch (e: any) {
|
||||
const text = e.message.toString();
|
||||
} catch (e: unknown) {
|
||||
const error = e instanceof Error ? e : new Error(String(e));
|
||||
const text = error.message.toString();
|
||||
|
||||
if (text.includes("is not defined")) {
|
||||
await oldSendMessage(msg, Environment.variableNotDefinedText).catch(logError);
|
||||
@@ -30,7 +31,7 @@ export class Ae extends Command {
|
||||
}
|
||||
|
||||
logError(`${text}
|
||||
* Stacktrace: ${e.stack}`);
|
||||
* Stacktrace: ${error.stack}`);
|
||||
|
||||
await oldSendMessage(msg, text).catch(logError);
|
||||
}
|
||||
@@ -43,15 +44,16 @@ export class Ae extends Command {
|
||||
e = ((typeof e == "string") ? e : JSON.stringify(e));
|
||||
|
||||
return e;
|
||||
} catch (e: any) {
|
||||
const text = e.message.toString();
|
||||
} catch (e: unknown) {
|
||||
const error = e instanceof Error ? e : new Error(String(e));
|
||||
const text = error.message.toString();
|
||||
|
||||
if (text.includes("is not defined")) {
|
||||
return Environment.evaluationVariableNotDefinedText;
|
||||
}
|
||||
|
||||
logError(`${text}
|
||||
* Stacktrace: ${e.stack}`);
|
||||
* Stacktrace: ${error.stack}`);
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ import {Message} from "typescript-telegram-bot-api";
|
||||
import {logError, oldReplyToMessage, randomValue} from "../util/utils";
|
||||
import {prepareTelegramMarkdownV2} from "../util/markdown-v2-renderer";
|
||||
import {Environment} from "../common/environment";
|
||||
import {appLogger} from "../logging/logger";
|
||||
|
||||
const logger = appLogger.child("command:choice");
|
||||
|
||||
export class Choice extends Command {
|
||||
command = "choice";
|
||||
@@ -12,7 +15,7 @@ export class Choice extends Command {
|
||||
description = Environment.commandDescriptions.choice;
|
||||
|
||||
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
|
||||
console.log("match", match);
|
||||
logger.debug("execute", {chatId: msg.chat?.id, messageId: msg.message_id, match});
|
||||
|
||||
const payload = match?.[3] || "";
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ export class Distort extends Command {
|
||||
}),
|
||||
{method: "sendPhoto", chatId, chatType: msg.chat.type}
|
||||
);
|
||||
} catch (e: any) {
|
||||
} catch (e: unknown) {
|
||||
await oldReplyToMessage(
|
||||
msg, Environment.getDistortFailedText(e)
|
||||
).catch(logError);
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import {Command} from "../base/command";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {logError, replyToMessage} from "../util/utils";
|
||||
import {AiProvider} from "../model/ai-provider";
|
||||
import {formatRuntimeModelInfo} from "../ai/provider-model-runtime";
|
||||
import {Environment} from "../common/environment";
|
||||
import {AiProvider} from "../model/ai-provider";
|
||||
import {ProviderGetModelCommand} from "./provider-model-command";
|
||||
|
||||
export class GeminiGetModel extends Command {
|
||||
title = Environment.commandTitles.geminiGetModel;
|
||||
description = Environment.commandDescriptions.geminiGetModel;
|
||||
|
||||
async execute(msg: Message): Promise<void> {
|
||||
await replyToMessage({message: msg, text: await formatRuntimeModelInfo(AiProvider.GEMINI)}).catch(logError);
|
||||
export class GeminiGetModel extends ProviderGetModelCommand {
|
||||
constructor() {
|
||||
super({
|
||||
provider: AiProvider.GEMINI,
|
||||
title: Environment.commandTitles.geminiGetModel,
|
||||
description: Environment.commandDescriptions.geminiGetModel,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,13 @@
|
||||
import {Command} from "../base/command";
|
||||
import {Requirements} from "../base/requirements";
|
||||
import {Requirement} from "../base/requirement";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {escapeHtml, logError, replyToMessage} from "../util/utils";
|
||||
import {AiProvider} from "../model/ai-provider";
|
||||
import {listProviderModels} from "../ai/provider-model-runtime";
|
||||
import {Environment} from "../common/environment";
|
||||
import {AiProvider} from "../model/ai-provider";
|
||||
import {ProviderListModelsCommand} from "./provider-model-command";
|
||||
|
||||
export class GeminiListModels extends Command {
|
||||
title = Environment.commandTitles.geminiListModels;
|
||||
description = Environment.commandDescriptions.geminiListModels;
|
||||
|
||||
requirements = Requirements.Build(Requirement.BOT_CREATOR);
|
||||
|
||||
async execute(msg: Message): Promise<void> {
|
||||
try {
|
||||
const models = (await listProviderModels(AiProvider.GEMINI)).sort((a, b) => a.localeCompare(b));
|
||||
const modelsString = escapeHtml(models.join("\n").substring(0, 4000));
|
||||
const text = Environment.modelListHeaderText + "<blockquote expandable>" + modelsString + "</blockquote>";
|
||||
|
||||
await replyToMessage({message: msg, text, parse_mode: "HTML"});
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
await replyToMessage({message: msg, text: Environment.modelListLoadFailedText}).catch(logError);
|
||||
}
|
||||
export class GeminiListModels extends ProviderListModelsCommand {
|
||||
constructor() {
|
||||
super({
|
||||
provider: AiProvider.GEMINI,
|
||||
title: Environment.commandTitles.geminiListModels,
|
||||
description: Environment.commandDescriptions.geminiListModels,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,13 @@
|
||||
import {Command} from "../base/command";
|
||||
import {Requirements} from "../base/requirements";
|
||||
import {Requirement} from "../base/requirement";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {logError, replyToMessage} from "../util/utils";
|
||||
import {AiProvider} from "../model/ai-provider";
|
||||
import {getRuntimeModel, setRuntimeModel, formatRuntimeModelInfo} from "../ai/provider-model-runtime";
|
||||
import {Environment} from "../common/environment";
|
||||
import {AiProvider} from "../model/ai-provider";
|
||||
import {ProviderSetModelCommand} from "./provider-model-command";
|
||||
|
||||
export class GeminiSetModel extends Command {
|
||||
argsMode = "required" as const;
|
||||
|
||||
title = Environment.commandTitles.geminiSetModel;
|
||||
description = Environment.commandDescriptions.geminiSetModel;
|
||||
|
||||
requirements = Requirements.Build(Requirement.BOT_CREATOR);
|
||||
|
||||
async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> {
|
||||
const newModel = match?.[3]?.trim();
|
||||
if (newModel) setRuntimeModel(AiProvider.GEMINI, newModel);
|
||||
|
||||
const model = getRuntimeModel(AiProvider.GEMINI);
|
||||
const text = newModel
|
||||
? Environment.getSelectedModelWithInfoText(model, await formatRuntimeModelInfo(AiProvider.GEMINI))
|
||||
: Environment.getModelIsNotSetCurrentText(model);
|
||||
|
||||
await replyToMessage({message: msg, text}).catch(logError);
|
||||
export class GeminiSetModel extends ProviderSetModelCommand {
|
||||
constructor() {
|
||||
super({
|
||||
provider: AiProvider.GEMINI,
|
||||
title: Environment.commandTitles.geminiSetModel,
|
||||
description: Environment.commandDescriptions.geminiSetModel,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import {Command} from "../base/command";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {logError, replyToMessage} from "../util/utils";
|
||||
import {AiProvider} from "../model/ai-provider";
|
||||
import {formatRuntimeModelInfo} from "../ai/provider-model-runtime";
|
||||
import {Environment} from "../common/environment";
|
||||
import {AiProvider} from "../model/ai-provider";
|
||||
import {ProviderGetModelCommand} from "./provider-model-command";
|
||||
|
||||
export class MistralGetModel extends Command {
|
||||
title = Environment.commandTitles.mistralGetModel;
|
||||
description = Environment.commandDescriptions.mistralGetModel;
|
||||
|
||||
async execute(msg: Message): Promise<void> {
|
||||
await replyToMessage({message: msg, text: await formatRuntimeModelInfo(AiProvider.MISTRAL)}).catch(logError);
|
||||
export class MistralGetModel extends ProviderGetModelCommand {
|
||||
constructor() {
|
||||
super({
|
||||
provider: AiProvider.MISTRAL,
|
||||
title: Environment.commandTitles.mistralGetModel,
|
||||
description: Environment.commandDescriptions.mistralGetModel,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,13 @@
|
||||
import {Command} from "../base/command";
|
||||
import {Requirements} from "../base/requirements";
|
||||
import {Requirement} from "../base/requirement";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {escapeHtml, logError, replyToMessage} from "../util/utils";
|
||||
import {AiProvider} from "../model/ai-provider";
|
||||
import {listProviderModels} from "../ai/provider-model-runtime";
|
||||
import {Environment} from "../common/environment";
|
||||
import {AiProvider} from "../model/ai-provider";
|
||||
import {ProviderListModelsCommand} from "./provider-model-command";
|
||||
|
||||
export class MistralListModels extends Command {
|
||||
title = Environment.commandTitles.mistralListModels;
|
||||
description = Environment.commandDescriptions.mistralListModels;
|
||||
|
||||
requirements = Requirements.Build(Requirement.BOT_CREATOR);
|
||||
|
||||
async execute(msg: Message): Promise<void> {
|
||||
try {
|
||||
const models = (await listProviderModels(AiProvider.MISTRAL)).sort((a, b) => a.localeCompare(b));
|
||||
const modelsString = escapeHtml(models.join("\n").substring(0, 4000));
|
||||
const text = Environment.modelListHeaderText + "<blockquote expandable>" + modelsString + "</blockquote>";
|
||||
|
||||
await replyToMessage({message: msg, text, parse_mode: "HTML"});
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
await replyToMessage({message: msg, text: Environment.modelListLoadFailedText}).catch(logError);
|
||||
}
|
||||
export class MistralListModels extends ProviderListModelsCommand {
|
||||
constructor() {
|
||||
super({
|
||||
provider: AiProvider.MISTRAL,
|
||||
title: Environment.commandTitles.mistralListModels,
|
||||
description: Environment.commandDescriptions.mistralListModels,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,13 @@
|
||||
import {Command} from "../base/command";
|
||||
import {Requirements} from "../base/requirements";
|
||||
import {Requirement} from "../base/requirement";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {logError, replyToMessage} from "../util/utils";
|
||||
import {AiProvider} from "../model/ai-provider";
|
||||
import {getRuntimeModel, setRuntimeModel, formatRuntimeModelInfo} from "../ai/provider-model-runtime";
|
||||
import {Environment} from "../common/environment";
|
||||
import {AiProvider} from "../model/ai-provider";
|
||||
import {ProviderSetModelCommand} from "./provider-model-command";
|
||||
|
||||
export class MistralSetModel extends Command {
|
||||
argsMode = "required" as const;
|
||||
|
||||
title = Environment.commandTitles.mistralSetModel;
|
||||
description = Environment.commandDescriptions.mistralSetModel;
|
||||
|
||||
requirements = Requirements.Build(Requirement.BOT_CREATOR);
|
||||
|
||||
async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> {
|
||||
const newModel = match?.[3]?.trim();
|
||||
if (newModel) setRuntimeModel(AiProvider.MISTRAL, newModel);
|
||||
|
||||
const model = getRuntimeModel(AiProvider.MISTRAL);
|
||||
const text = newModel
|
||||
? Environment.getSelectedModelWithInfoText(model, await formatRuntimeModelInfo(AiProvider.MISTRAL))
|
||||
: Environment.getModelIsNotSetCurrentText(model);
|
||||
|
||||
await replyToMessage({message: msg, text}).catch(logError);
|
||||
export class MistralSetModel extends ProviderSetModelCommand {
|
||||
constructor() {
|
||||
super({
|
||||
provider: AiProvider.MISTRAL,
|
||||
title: Environment.commandTitles.mistralSetModel,
|
||||
description: Environment.commandDescriptions.mistralSetModel,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import {Command} from "../base/command";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {logError, replyToMessage} from "../util/utils";
|
||||
import {AiProvider} from "../model/ai-provider";
|
||||
import {formatRuntimeModelInfo} from "../ai/provider-model-runtime";
|
||||
import {Environment} from "../common/environment";
|
||||
import {AiProvider} from "../model/ai-provider";
|
||||
import {ProviderGetModelCommand} from "./provider-model-command";
|
||||
|
||||
export class OllamaGetModel extends Command {
|
||||
title = Environment.commandTitles.ollamaGetModel;
|
||||
description = Environment.commandDescriptions.ollamaGetModel;
|
||||
|
||||
async execute(msg: Message): Promise<void> {
|
||||
await replyToMessage({message: msg, text: await formatRuntimeModelInfo(AiProvider.OLLAMA)}).catch(logError);
|
||||
export class OllamaGetModel extends ProviderGetModelCommand {
|
||||
constructor() {
|
||||
super({
|
||||
provider: AiProvider.OLLAMA,
|
||||
title: Environment.commandTitles.ollamaGetModel,
|
||||
description: Environment.commandDescriptions.ollamaGetModel,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,13 @@
|
||||
import {Command} from "../base/command";
|
||||
import {Requirements} from "../base/requirements";
|
||||
import {Requirement} from "../base/requirement";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {Environment} from "../common/environment";
|
||||
import {escapeHtml, logError, replyToMessage} from "../util/utils";
|
||||
import {AiProvider} from "../model/ai-provider";
|
||||
import {listProviderModels} from "../ai/provider-model-runtime";
|
||||
import {createOllamaClient, resolveAiRuntimeTarget} from "../ai/ai-runtime-target";
|
||||
import {ProviderListModelsCommand} from "./provider-model-command";
|
||||
|
||||
export class OllamaListModels extends Command {
|
||||
title = Environment.commandTitles.ollamaListModels;
|
||||
description = Environment.commandDescriptions.ollamaListModels;
|
||||
|
||||
requirements = Requirements.Build(Requirement.BOT_CREATOR);
|
||||
|
||||
async execute(msg: Message): Promise<void> {
|
||||
try {
|
||||
const target = resolveAiRuntimeTarget(AiProvider.OLLAMA, "chat");
|
||||
const models = (await listProviderModels(AiProvider.OLLAMA)).sort((a, b) => a.localeCompare(b));
|
||||
const modelsString = escapeHtml(models.join("\n").substring(0, 4000));
|
||||
const loadedModels = ((await createOllamaClient(target).ps())?.models ?? [])
|
||||
.map(model => model.model || model.name)
|
||||
.filter((model): model is string => !!model);
|
||||
const text =
|
||||
Environment.getLoadedModelsText(loadedModels) + "\n\n" +
|
||||
Environment.modelListHeaderText + "<blockquote expandable>" + modelsString + "</blockquote>";
|
||||
|
||||
await replyToMessage({message: msg, text, parse_mode: "HTML"});
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
await replyToMessage({message: msg, text: Environment.modelListLoadFailedText}).catch(logError);
|
||||
}
|
||||
export class OllamaListModels extends ProviderListModelsCommand {
|
||||
constructor() {
|
||||
super({
|
||||
provider: AiProvider.OLLAMA,
|
||||
title: Environment.commandTitles.ollamaListModels,
|
||||
description: Environment.commandDescriptions.ollamaListModels,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,13 @@
|
||||
import {Command} from "../base/command";
|
||||
import {Requirements} from "../base/requirements";
|
||||
import {Requirement} from "../base/requirement";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {logError, replyToMessage} from "../util/utils";
|
||||
import {AiProvider} from "../model/ai-provider";
|
||||
import {getRuntimeModel, setRuntimeModel, formatRuntimeModelInfo} from "../ai/provider-model-runtime";
|
||||
import {Environment} from "../common/environment";
|
||||
import {AiProvider} from "../model/ai-provider";
|
||||
import {ProviderSetModelCommand} from "./provider-model-command";
|
||||
|
||||
export class OllamaSetModel extends Command {
|
||||
argsMode = "required" as const;
|
||||
|
||||
title = Environment.commandTitles.ollamaSetModel;
|
||||
description = Environment.commandDescriptions.ollamaSetModel;
|
||||
|
||||
requirements = Requirements.Build(Requirement.BOT_CREATOR);
|
||||
|
||||
async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> {
|
||||
const newModel = match?.[3]?.trim();
|
||||
if (newModel) setRuntimeModel(AiProvider.OLLAMA, newModel);
|
||||
|
||||
const model = getRuntimeModel(AiProvider.OLLAMA);
|
||||
const text = newModel
|
||||
? Environment.getSelectedModelWithInfoText(model, await formatRuntimeModelInfo(AiProvider.OLLAMA))
|
||||
: Environment.getModelIsNotSetCurrentText(model);
|
||||
|
||||
await replyToMessage({message: msg, text}).catch(logError);
|
||||
export class OllamaSetModel extends ProviderSetModelCommand {
|
||||
constructor() {
|
||||
super({
|
||||
provider: AiProvider.OLLAMA,
|
||||
title: Environment.commandTitles.ollamaSetModel,
|
||||
description: Environment.commandDescriptions.ollamaSetModel,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import {Command} from "../base/command";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {logError, replyToMessage} from "../util/utils";
|
||||
import {AiProvider} from "../model/ai-provider";
|
||||
import {formatRuntimeModelInfo} from "../ai/provider-model-runtime";
|
||||
import {Environment} from "../common/environment";
|
||||
import {AiProvider} from "../model/ai-provider";
|
||||
import {ProviderGetModelCommand} from "./provider-model-command";
|
||||
|
||||
export class OpenAIGetModel extends Command {
|
||||
title = Environment.commandTitles.openAiGetModel;
|
||||
description = Environment.commandDescriptions.openAiGetModel;
|
||||
|
||||
async execute(msg: Message): Promise<void> {
|
||||
await replyToMessage({message: msg, text: await formatRuntimeModelInfo(AiProvider.OPENAI)}).catch(logError);
|
||||
export class OpenAIGetModel extends ProviderGetModelCommand {
|
||||
constructor() {
|
||||
super({
|
||||
provider: AiProvider.OPENAI,
|
||||
title: Environment.commandTitles.openAiGetModel,
|
||||
description: Environment.commandDescriptions.openAiGetModel,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,13 @@
|
||||
import {Command} from "../base/command";
|
||||
import {Requirements} from "../base/requirements";
|
||||
import {Requirement} from "../base/requirement";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {escapeHtml, logError, replyToMessage} from "../util/utils";
|
||||
import {AiProvider} from "../model/ai-provider";
|
||||
import {listProviderModels} from "../ai/provider-model-runtime";
|
||||
import {Environment} from "../common/environment";
|
||||
import {AiProvider} from "../model/ai-provider";
|
||||
import {ProviderListModelsCommand} from "./provider-model-command";
|
||||
|
||||
export class OpenAIListModels extends Command {
|
||||
title = Environment.commandTitles.openAiListModels;
|
||||
description = Environment.commandDescriptions.openAiListModels;
|
||||
|
||||
requirements = Requirements.Build(Requirement.BOT_CREATOR);
|
||||
|
||||
async execute(msg: Message): Promise<void> {
|
||||
try {
|
||||
const models = (await listProviderModels(AiProvider.OPENAI)).sort((a, b) => a.localeCompare(b));
|
||||
const modelsString = escapeHtml(models.join("\n").substring(0, 4000));
|
||||
const text = Environment.modelListHeaderText + "<blockquote expandable>" + modelsString + "</blockquote>";
|
||||
|
||||
await replyToMessage({message: msg, text, parse_mode: "HTML"});
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
await replyToMessage({message: msg, text: Environment.modelListLoadFailedText}).catch(logError);
|
||||
}
|
||||
export class OpenAIListModels extends ProviderListModelsCommand {
|
||||
constructor() {
|
||||
super({
|
||||
provider: AiProvider.OPENAI,
|
||||
title: Environment.commandTitles.openAiListModels,
|
||||
description: Environment.commandDescriptions.openAiListModels,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,13 @@
|
||||
import {Command} from "../base/command";
|
||||
import {Requirements} from "../base/requirements";
|
||||
import {Requirement} from "../base/requirement";
|
||||
import {Message} from "typescript-telegram-bot-api";
|
||||
import {logError, replyToMessage} from "../util/utils";
|
||||
import {AiProvider} from "../model/ai-provider";
|
||||
import {getRuntimeModel, setRuntimeModel, formatRuntimeModelInfo} from "../ai/provider-model-runtime";
|
||||
import {Environment} from "../common/environment";
|
||||
import {AiProvider} from "../model/ai-provider";
|
||||
import {ProviderSetModelCommand} from "./provider-model-command";
|
||||
|
||||
export class OpenAISetModel extends Command {
|
||||
argsMode = "required" as const;
|
||||
|
||||
title = Environment.commandTitles.openAiSetModel;
|
||||
description = Environment.commandDescriptions.openAiSetModel;
|
||||
|
||||
requirements = Requirements.Build(Requirement.BOT_CREATOR);
|
||||
|
||||
async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> {
|
||||
const newModel = match?.[3]?.trim();
|
||||
if (newModel) setRuntimeModel(AiProvider.OPENAI, newModel);
|
||||
|
||||
const model = getRuntimeModel(AiProvider.OPENAI);
|
||||
const text = newModel
|
||||
? Environment.getSelectedModelWithInfoText(model, await formatRuntimeModelInfo(AiProvider.OPENAI))
|
||||
: Environment.getModelIsNotSetCurrentText(model);
|
||||
|
||||
await replyToMessage({message: msg, text}).catch(logError);
|
||||
export class OpenAISetModel extends ProviderSetModelCommand {
|
||||
constructor() {
|
||||
super({
|
||||
provider: AiProvider.OPENAI,
|
||||
title: Environment.commandTitles.openAiSetModel,
|
||||
description: Environment.commandDescriptions.openAiSetModel,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ export class Qr extends Command {
|
||||
}),
|
||||
{method: "sendPhoto", chatId, chatType: msg.chat.type}
|
||||
);
|
||||
} catch (e: any) {
|
||||
} catch (e: unknown) {
|
||||
await replyToMessage({
|
||||
message: msg,
|
||||
text: Environment.getQrCodeFailedText(e)
|
||||
|
||||
+11
-5
@@ -21,6 +21,9 @@ import {enqueueTelegramApiCall} from "../util/telegram-api-queue";
|
||||
import {AsyncSemaphore} from "../util/async-lock";
|
||||
import {Environment} from "../common/environment";
|
||||
import {getLruMapValue, setLruMapValue} from "../util/lru-map";
|
||||
import {appLogger} from "../logging/logger";
|
||||
|
||||
const logger = appLogger.child("command:quote");
|
||||
|
||||
try {
|
||||
GlobalFonts.registerFromPath("./assets/Inter_18pt-Thin.ttf", "InterThin");
|
||||
@@ -54,6 +57,8 @@ export class Quote extends Command {
|
||||
if (!reply) return;
|
||||
|
||||
try {
|
||||
const startedAt = Date.now();
|
||||
logger.debug("execute.start", {chatId, messageId: msg.message_id, replyMessageId: reply.message_id});
|
||||
const quoteRaw = (msg.quote?.text ?? reply.text ?? reply.caption ?? "").trim();
|
||||
if (quoteRaw.length === 0) {
|
||||
await replyToMessage({message: msg, text: Environment.quoteMissingTextText}).catch(logError);
|
||||
@@ -74,7 +79,8 @@ export class Quote extends Command {
|
||||
},
|
||||
}),
|
||||
{method: "sendPhoto", chatId, chatType: msg.chat.type}
|
||||
).catch(logError);
|
||||
);
|
||||
logger.debug("execute.done", {chatId, messageId: msg.message_id, bytes: png.length, duration: logger.duration(startedAt)});
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
await replyToMessage({message: msg, text: Environment.quoteBuildFailedText}).catch(logError);
|
||||
@@ -146,14 +152,14 @@ async function loadCustomEmoji(customEmojiId: string): Promise<CanvasImage | nul
|
||||
});
|
||||
|
||||
if (!stickerSet || stickerSet.length === 0) {
|
||||
console.warn(`Custom emoji ${customEmojiId} not found`);
|
||||
logger.warn("custom_emoji.not_found", {customEmojiId});
|
||||
return null;
|
||||
}
|
||||
|
||||
const sticker = stickerSet[0];
|
||||
|
||||
if (sticker.is_animated || sticker.is_video) {
|
||||
console.warn(`Animated/video custom emoji ${customEmojiId} not supported`);
|
||||
logger.warn("custom_emoji.unsupported", {customEmojiId});
|
||||
return loadEmoji(sticker.emoji);
|
||||
}
|
||||
|
||||
@@ -171,7 +177,7 @@ async function loadCustomEmoji(customEmojiId: string): Promise<CanvasImage | nul
|
||||
setLruMapValue(customEmojiCache, customEmojiId, img, CUSTOM_EMOJI_CACHE_MAX_ENTRIES);
|
||||
return img;
|
||||
} catch (e) {
|
||||
console.warn(`Failed to load custom emoji ${customEmojiId}:`, e);
|
||||
logger.warn("custom_emoji.load_failed", {customEmojiId, error: e});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -521,7 +527,7 @@ async function drawLine(ctx: SKRSContext2D, line: Segment[], x: number, baseline
|
||||
ctx.drawImage(<Image>img, cx, y, emojiSize, emojiSize);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Failed to draw custom emoji:", e);
|
||||
logger.warn("custom_emoji.draw_failed", {error: e});
|
||||
|
||||
try {
|
||||
const img = await loadEmoji("😥");
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import {parse as parseDotEnv} from "dotenv";
|
||||
import {z} from "zod";
|
||||
import {appLogger} from "../logging/logger";
|
||||
|
||||
import {saveData} from "../db/database";
|
||||
import {Answers} from "../model/answers";
|
||||
@@ -168,6 +169,7 @@ const RuntimeEnvSchema = z.object({
|
||||
),
|
||||
|
||||
SYSTEM_PROMPT: optionalStringSchema,
|
||||
RANKER_TOOL_PROMPT: optionalStringSchema,
|
||||
USE_NAMES_IN_PROMPT: booleanWithDefaultSchema(false),
|
||||
USE_SYSTEM_PROMPT: booleanWithDefaultSchema(true),
|
||||
|
||||
@@ -234,7 +236,9 @@ export class Environment {
|
||||
|
||||
private static lastEnvMtimeMs: number | undefined;
|
||||
private static lastSystemPromptMtimeMs: number | undefined;
|
||||
private static lastRankerToolPromptMtimeMs: number | undefined;
|
||||
private static envSystemPrompt: string | undefined;
|
||||
private static envRankerToolPrompt: string | undefined;
|
||||
|
||||
static BOT_TOKEN: string = "";
|
||||
static TEST_ENVIRONMENT: boolean = false;
|
||||
@@ -273,6 +277,7 @@ export class Environment {
|
||||
static DEFAULT_AI_PROVIDER: AiProvider = AiProvider.OLLAMA;
|
||||
|
||||
static SYSTEM_PROMPT?: string;
|
||||
static RANKER_TOOL_PROMPT?: string;
|
||||
static USE_NAMES_IN_PROMPT: boolean = false;
|
||||
static USE_SYSTEM_PROMPT: boolean = true;
|
||||
static SEND_TIME_TOOK: boolean = false;
|
||||
@@ -1632,8 +1637,8 @@ export class Environment {
|
||||
private static getFileMtimeMs(filePath: string): number | undefined {
|
||||
try {
|
||||
return fs.statSync(filePath).mtimeMs;
|
||||
} catch (e: any) {
|
||||
if (e?.code === "ENOENT") {
|
||||
} catch (e: unknown) {
|
||||
if ((e as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1665,6 +1670,10 @@ export class Environment {
|
||||
return path.join(Environment.DATA_PATH, "SYSTEM_PROMPT.md");
|
||||
}
|
||||
|
||||
private static getRankerToolPromptPath(): string {
|
||||
return path.join(Environment.DATA_PATH, "TOOL_RANKER_PROMPT.md");
|
||||
}
|
||||
|
||||
private static readSystemPrompt(): string | undefined {
|
||||
const promptPath = Environment.getSystemPromptPath();
|
||||
|
||||
@@ -1676,10 +1685,25 @@ export class Environment {
|
||||
return prompt.length > 0 ? prompt : undefined;
|
||||
}
|
||||
|
||||
private static readRankerToolPromptPath(): string | undefined {
|
||||
const promptPath = Environment.getRankerToolPromptPath();
|
||||
|
||||
if (!fs.existsSync(promptPath)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const prompt = fs.readFileSync(promptPath, "utf8").trim();
|
||||
return prompt.length > 0 ? prompt : undefined;
|
||||
}
|
||||
|
||||
private static refreshSystemPrompt(): void {
|
||||
Environment.SYSTEM_PROMPT = Environment.readSystemPrompt() ?? Environment.envSystemPrompt;
|
||||
}
|
||||
|
||||
private static refreshRankerToolPrompt(): void {
|
||||
Environment.RANKER_TOOL_PROMPT = Environment.readRankerToolPromptPath() ?? Environment.envRankerToolPrompt;
|
||||
}
|
||||
|
||||
private static applyStartupEnv(env: StartupEnv): void {
|
||||
Environment.BOT_TOKEN = env.BOT_TOKEN;
|
||||
Environment.TEST_ENVIRONMENT = env.TEST_ENVIRONMENT;
|
||||
@@ -1722,7 +1746,9 @@ export class Environment {
|
||||
Environment.DEFAULT_AI_PROVIDER = env.DEFAULT_AI_PROVIDER;
|
||||
|
||||
Environment.envSystemPrompt = env.SYSTEM_PROMPT;
|
||||
Environment.envRankerToolPrompt = env.RANKER_TOOL_PROMPT;
|
||||
Environment.SYSTEM_PROMPT = env.SYSTEM_PROMPT;
|
||||
Environment.RANKER_TOOL_PROMPT = env.RANKER_TOOL_PROMPT;
|
||||
Environment.USE_NAMES_IN_PROMPT = env.USE_NAMES_IN_PROMPT;
|
||||
Environment.USE_SYSTEM_PROMPT = env.USE_SYSTEM_PROMPT;
|
||||
Environment.SEND_TIME_TOOK = env.SEND_TIME_TOOK ?? false;
|
||||
@@ -1783,18 +1809,22 @@ export class Environment {
|
||||
Environment.applyRuntimeEnv(runtimeEnv);
|
||||
|
||||
Environment.refreshSystemPrompt();
|
||||
Environment.refreshRankerToolPrompt();
|
||||
|
||||
Environment.lastEnvMtimeMs = Environment.getFileMtimeMs(Environment.ENV_FILE_PATH);
|
||||
Environment.lastSystemPromptMtimeMs = Environment.getFileMtimeMs(Environment.getSystemPromptPath());
|
||||
Environment.lastRankerToolPromptMtimeMs = Environment.getFileMtimeMs(Environment.getRankerToolPromptPath());
|
||||
}
|
||||
|
||||
static reloadRuntimeConfigIfChanged(): void {
|
||||
try {
|
||||
const envMtimeMs = Environment.getFileMtimeMs(Environment.ENV_FILE_PATH);
|
||||
const systemPromptMtimeMs = Environment.getFileMtimeMs(Environment.getSystemPromptPath());
|
||||
const rankerToolPromptMtimeMs = Environment.getFileMtimeMs(Environment.getRankerToolPromptPath());
|
||||
|
||||
const envChanged = envMtimeMs !== Environment.lastEnvMtimeMs;
|
||||
const systemPromptChanged = systemPromptMtimeMs !== Environment.lastSystemPromptMtimeMs;
|
||||
const rankerToolPromptChanged = rankerToolPromptMtimeMs !== Environment.lastRankerToolPromptMtimeMs;
|
||||
|
||||
Localization.reloadIfChanged();
|
||||
|
||||
@@ -1808,6 +1838,7 @@ export class Environment {
|
||||
|
||||
Environment.applyRuntimeEnv(runtimeEnv);
|
||||
Environment.refreshSystemPrompt();
|
||||
Environment.refreshRankerToolPrompt();
|
||||
Environment.lastEnvMtimeMs = envMtimeMs;
|
||||
}
|
||||
|
||||
@@ -1815,8 +1846,13 @@ export class Environment {
|
||||
Environment.refreshSystemPrompt();
|
||||
Environment.lastSystemPromptMtimeMs = systemPromptMtimeMs;
|
||||
}
|
||||
|
||||
if (rankerToolPromptChanged) {
|
||||
Environment.refreshRankerToolPrompt();
|
||||
Environment.lastRankerToolPromptMtimeMs = rankerToolPromptMtimeMs;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to reload runtime environment config", e);
|
||||
appLogger.child("environment").error("runtime_reload.failed", {error: e});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import {AsyncLocalStorage} from "node:async_hooks";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import {appLogger} from "../logging/logger";
|
||||
|
||||
const logger = appLogger.child("localization");
|
||||
|
||||
export const DEFAULT_LOCALE = "en";
|
||||
export const DEFAULT_LANGUAGE_CHOICE = "default";
|
||||
@@ -23,8 +26,8 @@ function normalizeLanguageCode(value: string | undefined | null): string | undef
|
||||
function readMtimeMs(filePath: string): number | undefined {
|
||||
try {
|
||||
return fs.statSync(filePath).mtimeMs;
|
||||
} catch (e: any) {
|
||||
if (e?.code === "ENOENT") return undefined;
|
||||
} catch (e: unknown) {
|
||||
if ((e as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -235,7 +238,7 @@ export class Localization {
|
||||
try {
|
||||
bundles.set(locale, JSON.parse(fs.readFileSync(filePath, "utf8")) as LocalizationBundle);
|
||||
} catch (e) {
|
||||
console.error(`Failed to load localization file ${filePath}`, e);
|
||||
logger.error("file_load.failed", {filePath, locale, error: e});
|
||||
const previous = Localization.bundles.get(locale);
|
||||
if (previous) bundles.set(locale, previous);
|
||||
}
|
||||
@@ -244,8 +247,9 @@ export class Localization {
|
||||
Localization.bundles = bundles;
|
||||
Localization.fileMtimeMs = mtimes;
|
||||
Localization.fileSignature = signature;
|
||||
logger.debug("reload.done", {force, locales: [...bundles.keys()]});
|
||||
} catch (e) {
|
||||
console.error("Failed to reload localization files", e);
|
||||
logger.error("reload.failed", {error: e});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,13 @@ import {StoredMessage} from "../model/stored-message";
|
||||
import {and, eq} from "drizzle-orm";
|
||||
import {inArray} from "drizzle-orm/sql/expressions/conditions";
|
||||
import {Dao} from "../base/dao";
|
||||
import {appLogger} from "../logging/logger";
|
||||
import {buildExcludedSet} from "../util/utils";
|
||||
import {StoredAttachment} from "../model/stored-attachment";
|
||||
|
||||
export class MessageDao extends Dao<StoredMessage> {
|
||||
|
||||
private tag: string = "MessageDao";
|
||||
private readonly logger = appLogger.child("dao:messages");
|
||||
|
||||
override async getAll(): Promise<StoredMessage[]> {
|
||||
await DatabaseManager.ready;
|
||||
@@ -19,7 +20,7 @@ export class MessageDao extends Dao<StoredMessage> {
|
||||
|
||||
const now = Date.now();
|
||||
const diff = now - then;
|
||||
console.log(`${this.tag}: getAll()`, `took ${diff}ms; size: ${messages.length}`);
|
||||
this.logger.trace("get_all", {dao: "messages", duration: `${diff}ms`, size: messages.length});
|
||||
|
||||
return this.mapFrom(messages);
|
||||
}
|
||||
@@ -40,7 +41,7 @@ export class MessageDao extends Dao<StoredMessage> {
|
||||
|
||||
const now = Date.now();
|
||||
const diff = now - then;
|
||||
console.log(`${this.tag}: getById(${params.chatId}, ${params.id})`, `took ${diff}ms; size: ${messages.length}`);
|
||||
this.logger.trace("get_by_id", {dao: "messages", chatId: params.chatId, id: params.id, duration: `${diff}ms`, size: messages.length});
|
||||
|
||||
const m = messages[0];
|
||||
if (!m) return null;
|
||||
@@ -63,7 +64,7 @@ export class MessageDao extends Dao<StoredMessage> {
|
||||
|
||||
const now = Date.now();
|
||||
const diff = now - then;
|
||||
console.log(`${this.tag}: getByIds(${params.chatId}, ${params.ids})`, `took ${diff}ms; size: ${messages.length}`);
|
||||
this.logger.trace("get_by_ids", {dao: "messages", chatId: params.chatId, ids: params.ids, duration: `${diff}ms`, size: messages.length});
|
||||
|
||||
return this.mapFrom(messages);
|
||||
}
|
||||
@@ -83,7 +84,7 @@ export class MessageDao extends Dao<StoredMessage> {
|
||||
|
||||
const now = Date.now();
|
||||
const diff = now - then;
|
||||
console.log(`${this.tag}: insert(size: ${values.length})`, `took ${diff}ms'; inserted: ${r.rowsAffected}`);
|
||||
this.logger.debug("insert", {dao: "messages", duration: `${diff}ms`, size: values.length, rowsAffected: r.rowsAffected});
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+6
-5
@@ -1,5 +1,6 @@
|
||||
import {StoredUser} from "../model/stored-user";
|
||||
import {Dao} from "../base/dao";
|
||||
import {appLogger} from "../logging/logger";
|
||||
import {DatabaseManager} from "./database-manager";
|
||||
import {UserInsert, usersTable} from "./schema";
|
||||
import {eq} from "drizzle-orm";
|
||||
@@ -9,7 +10,7 @@ import {boolToInt, buildExcludedSet} from "../util/utils";
|
||||
|
||||
export class UserDao extends Dao<StoredUser> {
|
||||
|
||||
private tag: string = "UserDao";
|
||||
private readonly logger = appLogger.child("dao:users");
|
||||
|
||||
override async getAll(): Promise<StoredUser[]> {
|
||||
await DatabaseManager.ready;
|
||||
@@ -19,7 +20,7 @@ export class UserDao extends Dao<StoredUser> {
|
||||
|
||||
const now = Date.now();
|
||||
const diff = now - then;
|
||||
console.log(`${this.tag}: getAll()`, `took ${diff}ms; size: ${users.length}`);
|
||||
this.logger.trace("get_all", {dao: "users", duration: `${diff}ms`, size: users.length});
|
||||
|
||||
return this.mapFrom(users);
|
||||
}
|
||||
@@ -37,7 +38,7 @@ export class UserDao extends Dao<StoredUser> {
|
||||
|
||||
const now = Date.now();
|
||||
const diff = now - then;
|
||||
console.log(`${this.tag}: getById(${params.id})`, `took ${diff}ms; size: ${users.length}`);
|
||||
this.logger.trace("get_by_id", {dao: "users", id: params.id, duration: `${diff}ms`, size: users.length});
|
||||
|
||||
const u = users[0];
|
||||
if (!u) return null;
|
||||
@@ -57,7 +58,7 @@ export class UserDao extends Dao<StoredUser> {
|
||||
|
||||
const now = Date.now();
|
||||
const diff = now - then;
|
||||
console.log(`${this.tag}: getByIds(${params.ids})`, `took ${diff}ms; size: ${users.length}`);
|
||||
this.logger.trace("get_by_ids", {dao: "users", ids: params.ids, duration: `${diff}ms`, size: users.length});
|
||||
|
||||
return this.mapFrom(users);
|
||||
}
|
||||
@@ -78,7 +79,7 @@ export class UserDao extends Dao<StoredUser> {
|
||||
|
||||
const now = Date.now();
|
||||
const diff = now - then;
|
||||
console.log(`${this.tag}: insert(size: ${rows.length})`, `took ${diff}ms; inserted: ${r.rowsAffected}`);
|
||||
this.logger.debug("insert", {dao: "users", duration: `${diff}ms`, size: rows.length, rowsAffected: r.rowsAffected});
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+30
-12
@@ -1,4 +1,5 @@
|
||||
import "dotenv/config";
|
||||
import {appLogger} from "./logging/logger";
|
||||
import {Environment} from "./common/environment";
|
||||
import {BotCommand, TelegramBot, User} from "typescript-telegram-bot-api";
|
||||
import {Command} from "./base/command";
|
||||
@@ -189,13 +190,20 @@ export const videoDir = path.join(cacheDir, "video");
|
||||
export const videoNotesDir = path.join(cacheDir, "video-note");
|
||||
export const videoTempDir = path.join(videoDir, "temp");
|
||||
|
||||
|
||||
export const NOTES_HEADER = "## Notes\n";
|
||||
export const notesDir = path.join(Environment.DATA_PATH, "notes");
|
||||
export const notesRootFile = path.join(notesDir, "index.md");
|
||||
|
||||
const logger = appLogger.child("main");
|
||||
|
||||
let isShuttingDown = false;
|
||||
|
||||
async function shutdown(signal: NodeJS.Signals) {
|
||||
if (isShuttingDown) return;
|
||||
isShuttingDown = true;
|
||||
|
||||
console.log(`Received ${signal}. Stopping bot polling...`);
|
||||
logger.warn("shutdown.signal", {signal});
|
||||
|
||||
try {
|
||||
await bot.stopPolling();
|
||||
@@ -209,21 +217,33 @@ async function shutdown(signal: NodeJS.Signals) {
|
||||
async function main() {
|
||||
const start = Date.now();
|
||||
|
||||
console.log(
|
||||
`TEST_ENVIRONMENT: ${Environment.TEST_ENVIRONMENT}\n` +
|
||||
`DATA_PATH: ${Environment.DATA_PATH}\n` +
|
||||
`MAX_PHOTO_SIZE: ${Environment.MAX_PHOTO_SIZE}\n` +
|
||||
`ONLY_FOR_CREATOR: ${Environment.ONLY_FOR_CREATOR_MODE}\n` +
|
||||
`DEFAULT_AI_PROVIDER: ${Environment.DEFAULT_AI_PROVIDER}`
|
||||
);
|
||||
logger.info("startup.config", {
|
||||
testEnvironment: Environment.TEST_ENVIRONMENT,
|
||||
isDocker: Environment.IS_DOCKER,
|
||||
dataPath: Environment.DATA_PATH,
|
||||
dbPath: Environment.DB_PATH,
|
||||
});
|
||||
|
||||
const dirsToCheck = [cacheDir, photoDir, photoGenDir, documentDir, audioDir, videoDir, videoNotesDir, videoTempDir];
|
||||
const dirsToCheck = [cacheDir, photoDir, photoGenDir, documentDir, audioDir, videoDir, videoNotesDir, videoTempDir, notesDir];
|
||||
dirsToCheck.forEach(dir => {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, {recursive: true});
|
||||
logger.debug("startup.dir_created", {dir});
|
||||
}
|
||||
});
|
||||
|
||||
const notesRootFilePath = path.join(notesDir, "index.md");
|
||||
if (!fs.existsSync(notesRootFilePath)) {
|
||||
fs.writeFileSync(notesRootFilePath, "\n" + NOTES_HEADER);
|
||||
logger.debug("startup.notes_root_created", {notesRootFilePath});
|
||||
}
|
||||
|
||||
if (!(fs.readFileSync(notesRootFilePath).toString().includes(NOTES_HEADER))) {
|
||||
fs.appendFileSync(notesRootFilePath, "\n" + NOTES_HEADER);
|
||||
logger.debug("startup.notes_header_added", {notesRootFilePath});
|
||||
}
|
||||
|
||||
// TODO: 13/05/2026, Danil Nikolaev: maybe add clean cache option (or just save summarizations)
|
||||
// const now = new Date();
|
||||
|
||||
// const midnight = new Date();
|
||||
@@ -231,12 +251,10 @@ async function main() {
|
||||
// midnight.setDate(now.getDate() + 1);
|
||||
|
||||
// const diff = midnight.getTime() - now.getTime();
|
||||
// console.log("Clearing up cache will be started in " + diff + "ms");
|
||||
|
||||
// clearUpFolderFromOldFiles(cacheDir);
|
||||
// delay(diff).then(() => {
|
||||
// setInterval(() => {
|
||||
// console.log("Started clearing up cache");
|
||||
// clearUpFolderFromOldFiles(cacheDir);
|
||||
// }, 1000 * 60 * 60 * 24);
|
||||
// });
|
||||
@@ -264,7 +282,7 @@ async function main() {
|
||||
|
||||
const end = Date.now();
|
||||
const diff = Math.abs(end - start);
|
||||
console.log(`Bot started in ${diff}ms!`);
|
||||
logger.success("startup.ready", {duration: `${diff}ms`, commands: cmds.length, botId: botUser.id, botUsername: botUser.username});
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export type OllamaRequest = {
|
||||
uuid: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
stream: any;
|
||||
stream: unknown;
|
||||
done: boolean;
|
||||
fromId: number;
|
||||
chatId: number;
|
||||
|
||||
+10
-2
@@ -1,6 +1,9 @@
|
||||
import {logError} from "./utils";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import {appLogger} from "../logging/logger";
|
||||
|
||||
const logger = appLogger.child("files");
|
||||
|
||||
export function clearUpFolderFromOldFiles(folder: string, recursive = true) {
|
||||
fs.readdir(folder, (err, files) => {
|
||||
@@ -33,11 +36,16 @@ export function clearUpFolderFromOldFiles(folder: string, recursive = true) {
|
||||
}
|
||||
});
|
||||
|
||||
console.log("filenamesToDelete", filenamesToDelete);
|
||||
logger.debug("cleanup.candidates", {folder, recursive, count: filenamesToDelete.length, filenamesToDelete});
|
||||
if (filenamesToDelete.length) {
|
||||
filenamesToDelete.forEach((filename) => {
|
||||
fs.rm(filename, (e) => {
|
||||
if (e) logError(e);
|
||||
if (e) {
|
||||
logger.error("cleanup.delete_failed", {filename, error: e});
|
||||
logError(e);
|
||||
} else {
|
||||
logger.debug("cleanup.deleted", {filename});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import {exec} from "node:child_process";
|
||||
import {promisify} from "node:util";
|
||||
import {appLogger} from "../logging/logger";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
const logger = appLogger.child("shell-command-runner");
|
||||
|
||||
export type ShellCommandResult = {
|
||||
stdout: string | null | undefined;
|
||||
@@ -60,19 +62,19 @@ export class ShellCommandRunner {
|
||||
maxBuffer: 64 * 1024,
|
||||
});
|
||||
if (stdout) {
|
||||
console.log("COMMAND: ", command, "\n", "Output:", stdout);
|
||||
logger.debug("command.stdout", {command, stdout});
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.error("COMMAND: ", command, "\n", "Error:", stderr);
|
||||
logger.warn("command.stderr", {command, stderr});
|
||||
}
|
||||
|
||||
return {stdout, stderr};
|
||||
} catch (error: any) {
|
||||
console.error("Error code:", error.code);
|
||||
console.error("Stderr:", error.stderr);
|
||||
} catch (error: unknown) {
|
||||
const err = error as Partial<Error & {code: unknown; stdout: string; stderr: string}>;
|
||||
logger.error("command.failed", {command, code: err.code, stderr: err.stderr, error});
|
||||
|
||||
return {stdout: error.stdout ?? null, stderr: error.stderr ?? error.message};
|
||||
return {stdout: err.stdout ?? null, stderr: err.stderr ?? err.message ?? String(error)};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
* queue always honors `parameters.retry_after` and requeues the task.
|
||||
*/
|
||||
|
||||
import {appLogger} from "../logging/logger";
|
||||
|
||||
const logger = appLogger.child("telegram-api-queue");
|
||||
|
||||
export type TelegramChatId = number | string;
|
||||
|
||||
export type TelegramChatType = string;
|
||||
@@ -345,6 +349,7 @@ export class TelegramApiQueue {
|
||||
this.globalBucket = new SlidingWindowRateLimit(this.options.globalLimit);
|
||||
this.editBucket = new SlidingWindowRateLimit(this.options.editLimit);
|
||||
this.bucketIdleMs = Math.max(this.options.perChatLimit.intervalMs, this.options.groupChatLimit.intervalMs) * 2;
|
||||
logger.debug("created", {maxConcurrent: this.options.maxConcurrent, maxAttempts: this.options.maxAttempts, maxQueueSize: this.options.maxQueueSize});
|
||||
}
|
||||
|
||||
get stats(): TelegramApiQueueStats {
|
||||
@@ -356,9 +361,18 @@ export class TelegramApiQueue {
|
||||
}
|
||||
|
||||
enqueue<T>(task: TelegramApiTask<T>, options: TelegramApiQueueTaskOptions = {}): Promise<T> {
|
||||
if (this.closed) return Promise.reject(createClosedError());
|
||||
if (this.queue.length >= this.options.maxQueueSize) return Promise.reject(createQueueOverflowError(this.options.maxQueueSize));
|
||||
if (options.signal?.aborted) return Promise.reject(createAbortError());
|
||||
if (this.closed) {
|
||||
logger.warn("enqueue.rejected.closed", {method: options.method, chatId: options.chatId});
|
||||
return Promise.reject(createClosedError());
|
||||
}
|
||||
if (this.queue.length >= this.options.maxQueueSize) {
|
||||
logger.error("enqueue.rejected.overflow", {method: options.method, chatId: options.chatId, queued: this.queue.length, maxQueueSize: this.options.maxQueueSize});
|
||||
return Promise.reject(createQueueOverflowError(this.options.maxQueueSize));
|
||||
}
|
||||
if (options.signal?.aborted) {
|
||||
logger.debug("enqueue.rejected.aborted", {method: options.method, chatId: options.chatId});
|
||||
return Promise.reject(createAbortError());
|
||||
}
|
||||
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const entry: QueueEntry<T> = {
|
||||
@@ -376,6 +390,7 @@ export class TelegramApiQueue {
|
||||
this.attachAbortHandler(entry);
|
||||
|
||||
this.insertEntry(entry as QueueEntry<unknown>);
|
||||
logger.trace("enqueue.accepted", {taskId: entry.id, method: options.method, chatId: options.chatId, priority: options.priority, queued: this.queue.length, running: this.running});
|
||||
this.pump();
|
||||
});
|
||||
}
|
||||
@@ -390,12 +405,14 @@ export class TelegramApiQueue {
|
||||
|
||||
close(reason: unknown = createClosedError()): void {
|
||||
this.closed = true;
|
||||
logger.warn("closed", {queued: this.queue.length, running: this.running, reason});
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
|
||||
const queued = this.queue;
|
||||
logger.debug("close.cancel_queued", {queued: queued.length});
|
||||
this.queue = [];
|
||||
for (const entry of queued) {
|
||||
this.cleanupAbortHandler(entry);
|
||||
@@ -409,6 +426,7 @@ export class TelegramApiQueue {
|
||||
|
||||
clear(reason: unknown = new Error("Telegram API queue was cleared")): void {
|
||||
const queued = this.queue;
|
||||
logger.warn("cleared", {queued: queued.length, running: this.running, reason});
|
||||
this.queue = [];
|
||||
for (const entry of queued) {
|
||||
this.cleanupAbortHandler(entry);
|
||||
@@ -435,6 +453,7 @@ export class TelegramApiQueue {
|
||||
|
||||
this.cleanupAbortHandler(entry);
|
||||
entry.state = "cancelled";
|
||||
logger.debug("entry.cancelled", {taskId: entry.id, method: entry.options.method, chatId: entry.options.chatId});
|
||||
entry.reject(createAbortError());
|
||||
this.resolveIdleIfNeeded();
|
||||
}
|
||||
@@ -456,6 +475,7 @@ export class TelegramApiQueue {
|
||||
}
|
||||
|
||||
if (selection.delayMs > 0) {
|
||||
logger.trace("pump.delayed", {delayMs: selection.delayMs, queued: this.queue.length, running: this.running});
|
||||
this.schedule(selection.delayMs);
|
||||
return;
|
||||
}
|
||||
@@ -497,6 +517,7 @@ export class TelegramApiQueue {
|
||||
this.cleanupAbortHandler(entry);
|
||||
this.recordStart(entry, Date.now());
|
||||
this.running++;
|
||||
logger.trace("entry.started", {taskId: entry.id, method: entry.options.method, chatId: entry.options.chatId, attempt: entry.attempt, queued: this.queue.length, running: this.running});
|
||||
void this.runEntry(entry);
|
||||
}
|
||||
|
||||
@@ -509,6 +530,7 @@ export class TelegramApiQueue {
|
||||
signal: entry.options.signal,
|
||||
});
|
||||
entry.state = "settled";
|
||||
logger.trace("entry.settled", {taskId: entry.id, method: entry.options.method, chatId: entry.options.chatId, attempt: entry.attempt});
|
||||
entry.resolve(result);
|
||||
} catch (error) {
|
||||
const retry = this.getRetryDecision(error, entry);
|
||||
@@ -523,6 +545,7 @@ export class TelegramApiQueue {
|
||||
} else {
|
||||
this.attachAbortHandler(entry);
|
||||
this.insertEntry(entry);
|
||||
logger.warn("entry.retry", {taskId: entry.id, method: entry.options.method, chatId: entry.options.chatId, attempt: entry.attempt - 1, delayMs: retry.delayMs, reason: retry.reason, error});
|
||||
this.options.onRetry?.({
|
||||
taskId: entry.id,
|
||||
method: entry.options.method,
|
||||
@@ -535,6 +558,7 @@ export class TelegramApiQueue {
|
||||
}
|
||||
} else {
|
||||
entry.state = "settled";
|
||||
logger.error("entry.failed", {taskId: entry.id, method: entry.options.method, chatId: entry.options.chatId, attempt: entry.attempt, error});
|
||||
entry.reject(this.closed ? createClosedError() : error);
|
||||
}
|
||||
} finally {
|
||||
|
||||
+41
-24
@@ -1,4 +1,5 @@
|
||||
import * as si from "systeminformation";
|
||||
import {appLogger} from "../logging/logger";
|
||||
import {Command} from "../base/command";
|
||||
import {CallbackCommand} from "../base/callback-command";
|
||||
import {
|
||||
@@ -48,6 +49,9 @@ import {ShellCommandResult, ShellCommandRunner} from "./shell-command-runner";
|
||||
|
||||
const imageProcessingSemaphore = new AsyncSemaphore(2);
|
||||
const fileWriteLocks = new KeyedAsyncLock();
|
||||
const logger = appLogger.child("utils");
|
||||
const requirementLogger = appLogger.child("requirements");
|
||||
const messageLogger = appLogger.child("messages");
|
||||
|
||||
export const ignore = () => {
|
||||
};
|
||||
@@ -65,7 +69,7 @@ export const ignoreIfMarkupFailed = (e: Error | TelegramError) => {
|
||||
};
|
||||
|
||||
export const logError = (e: Error | TelegramError | string | unknown) => {
|
||||
console.error(e);
|
||||
appLogger.error("error", {error: e});
|
||||
};
|
||||
|
||||
export const errorPlaceholder = async (msg: Message) => {
|
||||
@@ -147,7 +151,7 @@ export async function checkRequirements(cmd: Command | CallbackCommand | null, m
|
||||
!Environment.CHAT_IDS_WHITELIST.has(chatId) &&
|
||||
!Environment.ADMIN_IDS.has(chatId) &&
|
||||
!Environment.ADMIN_IDS.has(fromId)) {
|
||||
console.log(`${title}: chatId whitelist ignored.`);
|
||||
requirementLogger.debug("rejected.chat_whitelist", {title, chatId, fromId});
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -171,19 +175,19 @@ export async function checkRequirements(cmd: Command | CallbackCommand | null, m
|
||||
};
|
||||
|
||||
if (reqs.isRequiresBotCreator() && fromId !== Environment.CREATOR_ID) {
|
||||
console.log(`${title}: creatorId is bad`);
|
||||
requirementLogger.debug("rejected.creator", {title, fromId});
|
||||
await notifyUser(Environment.notBotCreatorText);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (reqs.isRequiresBotAdmin() && !Environment.ADMIN_IDS.has(fromId)) {
|
||||
console.log(`${title}: adminId is bad`);
|
||||
requirementLogger.debug("rejected.bot_admin", {title, fromId});
|
||||
await notifyUser(Environment.notBotAdministratorText);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (reqs.isRequiresChat() && msg?.chat?.type === "private") {
|
||||
console.log(`${title}: chatId is bad`);
|
||||
requirementLogger.debug("rejected.chat_required", {title, chatId, chatType});
|
||||
await notifyUser(Environment.notAChatText);
|
||||
return false;
|
||||
}
|
||||
@@ -192,7 +196,7 @@ export async function checkRequirements(cmd: Command | CallbackCommand | null, m
|
||||
const member = await bot.getChatMember({chat_id: chatId, user_id: fromId});
|
||||
|
||||
if (!isMemberAdmin(member)) {
|
||||
console.log(`${title}: chatAdminId is bad`);
|
||||
requirementLogger.debug("rejected.chat_admin", {title, chatId, fromId});
|
||||
await notifyUser(Environment.notChatAdministratorText);
|
||||
return false;
|
||||
}
|
||||
@@ -202,14 +206,14 @@ export async function checkRequirements(cmd: Command | CallbackCommand | null, m
|
||||
const member = await bot.getChatMember({chat_id: chatId, user_id: botUser.id});
|
||||
|
||||
if (!isMemberAdmin(member)) {
|
||||
console.log(`${title}: botChatAdminId is bad`);
|
||||
requirementLogger.debug("rejected.bot_chat_admin", {title, chatId});
|
||||
await notifyUser(Environment.botNotChatAdministratorText);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (reqs.isRequiresReply() && !msg?.reply_to_message) {
|
||||
console.log(`${title}: replyMessage is bad`);
|
||||
requirementLogger.debug("rejected.reply_required", {title, chatId, messageId});
|
||||
await notifyUser(Environment.replyRequiredText);
|
||||
return false;
|
||||
}
|
||||
@@ -241,7 +245,7 @@ export async function checkRequirements(cmd: Command | CallbackCommand | null, m
|
||||
}
|
||||
|
||||
if (!originalFromId || (fromId !== originalFromId && fromId !== Environment.CREATOR_ID)) {
|
||||
console.log(`${title}: sameUser is bad`);
|
||||
requirementLogger.debug("rejected.same_user", {title, chatId, fromId, originalFromId});
|
||||
await notifyUser(Environment.onlyOriginalAuthorText);
|
||||
return false;
|
||||
}
|
||||
@@ -307,13 +311,13 @@ export async function editMessageText(options: EditOptions, retries = 1) {
|
||||
}
|
||||
);
|
||||
return Promise.resolve(message);
|
||||
} catch (e: any) {
|
||||
} catch (e: unknown) {
|
||||
logError(e);
|
||||
|
||||
if (isMarkupFailed(e)) {
|
||||
if (isMarkupFailed(e as Error | TelegramError)) {
|
||||
return Promise.resolve(true);
|
||||
} else if (isTooManyRequests(e) && retries > 0) {
|
||||
const retryAfter = Number(e.message.split("retry after ")[1]) || 30;
|
||||
} else if (isTooManyRequests(e as Error | TelegramError) && retries > 0) {
|
||||
const retryAfter = Number((e instanceof Error ? e.message : String(e)).split("retry after ")[1]) || 30;
|
||||
await delay(retryAfter * 1000);
|
||||
return editMessageText(options, retries - 1);
|
||||
} else {
|
||||
@@ -1836,9 +1840,10 @@ export function startIntervalEditor(params: {
|
||||
try {
|
||||
await params.editFn(next);
|
||||
lastSent = next;
|
||||
} catch (e: any) {
|
||||
if ((e?.description ?? e?.message ?? "").includes("message is not modified")) return;
|
||||
logError("edit failed: " + e);
|
||||
} catch (e: unknown) {
|
||||
const description = e instanceof Error ? e.message : String(e);
|
||||
if (description.includes("message is not modified")) return;
|
||||
logError("edit failed: " + description);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1896,7 +1901,6 @@ type RuntimeInfo =
|
||||
| { runtime: "unknown"; version: string };
|
||||
|
||||
export function getRuntimeInfo(): RuntimeInfo {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const v = process.versions ?? {};
|
||||
|
||||
if (typeof v.bun === "string") {
|
||||
@@ -1906,7 +1910,6 @@ export function getRuntimeInfo(): RuntimeInfo {
|
||||
return {runtime: "node", version: v.node};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return {runtime: "unknown", version: String(process.version ?? "")};
|
||||
}
|
||||
|
||||
@@ -1957,7 +1960,6 @@ export async function imageToBase64(filePath: string, withMimeType: boolean = fa
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function ifTrue(exp?: string | number | boolean): boolean {
|
||||
if (!exp) return false;
|
||||
|
||||
@@ -2091,19 +2093,23 @@ export function photoPathByUniqueId(uniqueId: string): string {
|
||||
}
|
||||
|
||||
export async function processMyChatMember(u: ChatMemberUpdated): Promise<void> {
|
||||
console.log("my_chat_member", u);
|
||||
messageLogger.debug("my_chat_member", {update: u});
|
||||
}
|
||||
|
||||
export async function processGuestMessage(msg: Message): Promise<void> {
|
||||
// return processNewMessage(msg, true);
|
||||
console.log("NEW_GUEST_MESSAGE", msg);
|
||||
messageLogger.debug("guest_message.received", {message: msg});
|
||||
}
|
||||
|
||||
export async function processNewMessage(msg: Message, isGuest?: boolean): Promise<void> {
|
||||
console.log(isGuest ? "NEW_GUEST_MESSAGE" : "NEW_MESSAGE", msg);
|
||||
messageLogger.debug(isGuest ? "guest_message.received" : "message.received", {message: msg});
|
||||
|
||||
if (!msg.from) return;
|
||||
if (!msg.from) {
|
||||
messageLogger.debug("message.skipped.no_sender", {chatId: msg.chat?.id, messageId: msg.message_id});
|
||||
return;
|
||||
}
|
||||
|
||||
const startedAt = Date.now();
|
||||
const from = msg.from;
|
||||
Environment.reloadRuntimeConfigIfChanged();
|
||||
|
||||
@@ -2116,6 +2122,7 @@ export async function processNewMessage(msg: Message, isGuest?: boolean): Promis
|
||||
UserStore.put(from)
|
||||
]
|
||||
);
|
||||
messageLogger.debug("message.persisted", {chatId: msg.chat.id, messageId: msg.message_id, fromId: from.id, duration: logger.duration(startedAt)});
|
||||
|
||||
storedMsg = results[0];
|
||||
locale = await resolveInterfaceLocaleForUser(from.id, from.language_code);
|
||||
@@ -2194,7 +2201,15 @@ export async function processNewMessage(msg: Message, isGuest?: boolean): Promis
|
||||
const hasAudioAttachment = !!msg.voice || !!msg.audio || !!msg.document?.mime_type?.startsWith("audio/")
|
||||
|| !!msg.video_note;
|
||||
const hasImageAttachment = !!msg.photo?.length || !!msg.document?.mime_type?.startsWith("image/");
|
||||
if (executed || (!cmdText && !hasAudioAttachment && !hasImageAttachment)) return;
|
||||
if (executed) {
|
||||
messageLogger.debug("message.command_executed", {chatId: msg.chat.id, messageId: msg.message_id, command: cmd?.title});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cmdText && !hasAudioAttachment && !hasImageAttachment) {
|
||||
messageLogger.debug("message.skipped.empty", {chatId: msg.chat.id, messageId: msg.message_id});
|
||||
return;
|
||||
}
|
||||
|
||||
const hasConfiguredPrefix = Environment.BOT_PREFIX.length > 0;
|
||||
const startsWithPrefix = hasConfiguredPrefix && cmdText.toLowerCase().startsWith(Environment.BOT_PREFIX.toLowerCase());
|
||||
@@ -2224,12 +2239,14 @@ export async function processNewMessage(msg: Message, isGuest?: boolean): Promis
|
||||
});
|
||||
|
||||
if (!isReplyToBot && !hasPrefix && !hasBotMention && !hasAudioAttachment) {
|
||||
messageLogger.debug("message.skipped.not_addressed", {chatId: msg.chat.id, messageId: msg.message_id});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const provider = await resolveEffectiveAiProviderForUser(from.id);
|
||||
|
||||
messageLogger.info("ai.dispatch", {chatId: msg.chat.id, messageId: msg.message_id, fromId: from.id, provider});
|
||||
void runUnifiedAi({
|
||||
provider: provider,
|
||||
msg: msg,
|
||||
|
||||
+16
-8
@@ -1,24 +1,32 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext", // Modern resolution
|
||||
"rootDir": "src", // Limits scope
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"incremental": true, // HUGE performance boost
|
||||
"isolatedModules": true, // Ensures compatibility with fast runners
|
||||
"incremental": true,
|
||||
"isolatedModules": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"sourceMap": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"types": [
|
||||
"node",
|
||||
"bun"
|
||||
]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"] // Explicitly exclude build artifacts
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user