Compare commits
16 Commits
master
..
ai-summary
| Author | SHA1 | Date | |
|---|---|---|---|
| 1fa83635c0 | |||
| 37d7d11959 | |||
| 054b3a5c0d | |||
| 0cc3748866 | |||
| 4a582c0a15 | |||
| ce02bebf7b | |||
| 6b50b23467 | |||
| b6eed9a993 | |||
| 8ff85790cf | |||
| c7e8bba488 | |||
| 8edc368752 | |||
| ce400f993c | |||
| 19cc7a6f9f | |||
| 4abb7dba67 | |||
| 112541db28 | |||
| b47fd08cb7 |
@@ -1,39 +0,0 @@
|
|||||||
// Closes issues and prs whose authors/agents don't accept the ai policy
|
|
||||||
// https://github.com/searxng/searxng/blob/master/AI_POLICY.rst
|
|
||||||
|
|
||||||
module.exports = async ({ github, context }) => {
|
|
||||||
const item = context.payload.pull_request || context.payload.issue;
|
|
||||||
const body = item.body || '';
|
|
||||||
const kind = context.payload.pull_request ? 'pull request' : 'issue';
|
|
||||||
|
|
||||||
// https://github.com/searxng/searxng/pull/6476#discussion_r3683782481
|
|
||||||
const hasBox = /\[[Xx]\].*AI Policy/.test(body);
|
|
||||||
const hasRef = /\[AI Policy\]:\s*https:\/\/github\.com\/searxng\/searxng\/.*AI_POLICY/.test(body);
|
|
||||||
if (hasBox && hasRef) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { owner, repo } = context.repo;
|
|
||||||
await github.rest.issues.createComment({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
issue_number: item.number,
|
|
||||||
body:
|
|
||||||
'Hello! Thank you for your contribution.\n\n' +
|
|
||||||
`Unfortunately your ${kind} was closed as the AI Policy has not been accepted.\n\n` +
|
|
||||||
`Please open a new ${kind} after confirming your contribution aligns with our AI Policy.`,
|
|
||||||
});
|
|
||||||
await github.rest.issues.addLabels({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
issue_number: item.number,
|
|
||||||
labels: ['invalid:slop'],
|
|
||||||
});
|
|
||||||
await github.rest.issues.update({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
issue_number: item.number,
|
|
||||||
state: 'closed',
|
|
||||||
state_reason: 'not_planned',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
---
|
|
||||||
# yamllint disable rule:line-length
|
|
||||||
name: AI Policy
|
|
||||||
|
|
||||||
# Closes any new issues and PRs from people (or agents) who don't accept the AI Policy
|
|
||||||
|
|
||||||
# yamllint disable-line rule:truthy
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [opened]
|
|
||||||
pull_request_target:
|
|
||||||
types: [opened]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check:
|
|
||||||
name: Check AI Policy
|
|
||||||
# for issues with an author who has not contributed before
|
|
||||||
if: >-
|
|
||||||
github.event.sender.type != 'Bot' &&
|
|
||||||
contains(fromJSON('["NONE","FIRST_TIMER","FIRST_TIME_CONTRIBUTOR"]'),
|
|
||||||
github.event.issue.author_association ||
|
|
||||||
github.event.pull_request.author_association)
|
|
||||||
runs-on: ubuntu-26.04-arm
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
||||||
with:
|
|
||||||
persist-credentials: "false"
|
|
||||||
|
|
||||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const script = require('./.github/scripts/ai_policy.cjs');
|
|
||||||
await script({ github, context });
|
|
||||||
@@ -50,7 +50,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Login to GHCR
|
- name: Login to GHCR
|
||||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
uses: docker/login-action@06fb636fac595d6fb4b28a5dfcb21a6f5091859c # v4.5.0
|
||||||
with:
|
with:
|
||||||
registry: "ghcr.io"
|
registry: "ghcr.io"
|
||||||
username: "${{ github.repository_owner }}"
|
username: "${{ github.repository_owner }}"
|
||||||
@@ -110,7 +110,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Login to GHCR
|
- name: Login to GHCR
|
||||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
uses: docker/login-action@06fb636fac595d6fb4b28a5dfcb21a6f5091859c # v4.5.0
|
||||||
with:
|
with:
|
||||||
registry: "ghcr.io"
|
registry: "ghcr.io"
|
||||||
username: "${{ github.repository_owner }}"
|
username: "${{ github.repository_owner }}"
|
||||||
@@ -144,14 +144,14 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
uses: docker/login-action@06fb636fac595d6fb4b28a5dfcb21a6f5091859c # v4.5.0
|
||||||
with:
|
with:
|
||||||
registry: "docker.io"
|
registry: "docker.io"
|
||||||
username: "${{ secrets.DOCKER_USER }}"
|
username: "${{ secrets.DOCKER_USER }}"
|
||||||
password: "${{ secrets.DOCKER_TOKEN }}"
|
password: "${{ secrets.DOCKER_TOKEN }}"
|
||||||
|
|
||||||
- name: Login to GHCR
|
- name: Login to GHCR
|
||||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
uses: docker/login-action@06fb636fac595d6fb4b28a5dfcb21a6f5091859c # v4.5.0
|
||||||
with:
|
with:
|
||||||
registry: "ghcr.io"
|
registry: "ghcr.io"
|
||||||
username: "${{ github.repository_owner }}"
|
username: "${{ github.repository_owner }}"
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
|
|
||||||
- if: github.ref_name == 'master'
|
- if: github.ref_name == 'master'
|
||||||
name: Release
|
name: Release
|
||||||
uses: JamesIves/github-pages-deploy-action@fa24774553152dd7873cd16ebd8d959b010c5445 # v4.9.0
|
uses: JamesIves/github-pages-deploy-action@d92aa235d04922e8f08b40ce78cc5442fcfbfa2f # v4.8.0
|
||||||
with:
|
with:
|
||||||
folder: "dist/docs"
|
folder: "dist/docs"
|
||||||
branch: "gh-pages"
|
branch: "gh-pages"
|
||||||
|
|||||||
@@ -179,3 +179,4 @@ features or generally made SearXNG better:
|
|||||||
- Tommaso Colella `<https://github.com/gioleppe>`
|
- Tommaso Colella `<https://github.com/gioleppe>`
|
||||||
- @AgentScrubbles
|
- @AgentScrubbles
|
||||||
- Filip Mikina `<https://github.com/fiffek>`
|
- Filip Mikina `<https://github.com/fiffek>`
|
||||||
|
- Jason Witty `<https://github.com/jasonwitty>`
|
||||||
|
|||||||
Generated
+199
-290
@@ -15,22 +15,22 @@
|
|||||||
"swiped-events": "1.2.0"
|
"swiped-events": "1.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.5.5",
|
"@biomejs/biome": "2.5.3",
|
||||||
"@types/node": "^26.1.1",
|
"@types/node": "^26.1.1",
|
||||||
"browserslist": "^4.28.7",
|
"browserslist": "^4.28.6",
|
||||||
"browserslist-to-esbuild": "^2.1.1",
|
"browserslist-to-esbuild": "^2.1.1",
|
||||||
"edge.js": "^6.5.1",
|
"edge.js": "^6.5.1",
|
||||||
"less": "^4.8.0",
|
"less": "^4.6.7",
|
||||||
"mathjs": "^15.2.0",
|
"mathjs": "^15.2.0",
|
||||||
"sharp": "~0.35.3",
|
"sharp": "~0.35.3",
|
||||||
"sort-package-json": "^4.0.0",
|
"sort-package-json": "^4.0.0",
|
||||||
"stylelint": "^17.14.1",
|
"stylelint": "^17.14.0",
|
||||||
"stylelint-config-standard-less": "^4.1.0",
|
"stylelint-config-standard-less": "^4.1.0",
|
||||||
"stylelint-prettier": "^5.0.3",
|
"stylelint-prettier": "^5.0.3",
|
||||||
"svgo": "^4.0.2",
|
"svgo": "^4.0.2",
|
||||||
"typescript": "~7.0.2",
|
"typescript": "~7.0.2",
|
||||||
"vite": "^8.1.5",
|
"vite": "^8.1.4",
|
||||||
"vite-bundle-analyzer": "^1.3.9"
|
"vite-bundle-analyzer": "^1.3.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
@@ -69,9 +69,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/biome": {
|
"node_modules/@biomejs/biome": {
|
||||||
"version": "2.5.5",
|
"version": "2.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.3.tgz",
|
||||||
"integrity": "sha512-r1S8nFsAG1MY+vJFZALzIvwXAJv6ejDQ0mxP21Tgr9YK3ZFtjrvbBwDdNhx1rUqvccEIeNg20cYCNzl6Cr69pQ==",
|
"integrity": "sha512-MrJswFdei9EfDwwUy2tQrPDpK0AO+RmMFvBoaaJ6ayBc3sUbHdCE+XG5N8vp+5So41ZupZJQm0roHFFhMGVD7A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -85,20 +85,20 @@
|
|||||||
"url": "https://opencollective.com/biome"
|
"url": "https://opencollective.com/biome"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@biomejs/cli-darwin-arm64": "2.5.5",
|
"@biomejs/cli-darwin-arm64": "2.5.3",
|
||||||
"@biomejs/cli-darwin-x64": "2.5.5",
|
"@biomejs/cli-darwin-x64": "2.5.3",
|
||||||
"@biomejs/cli-linux-arm64": "2.5.5",
|
"@biomejs/cli-linux-arm64": "2.5.3",
|
||||||
"@biomejs/cli-linux-arm64-musl": "2.5.5",
|
"@biomejs/cli-linux-arm64-musl": "2.5.3",
|
||||||
"@biomejs/cli-linux-x64": "2.5.5",
|
"@biomejs/cli-linux-x64": "2.5.3",
|
||||||
"@biomejs/cli-linux-x64-musl": "2.5.5",
|
"@biomejs/cli-linux-x64-musl": "2.5.3",
|
||||||
"@biomejs/cli-win32-arm64": "2.5.5",
|
"@biomejs/cli-win32-arm64": "2.5.3",
|
||||||
"@biomejs/cli-win32-x64": "2.5.5"
|
"@biomejs/cli-win32-x64": "2.5.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-darwin-arm64": {
|
"node_modules/@biomejs/cli-darwin-arm64": {
|
||||||
"version": "2.5.5",
|
"version": "2.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.3.tgz",
|
||||||
"integrity": "sha512-kUrAhXVWUrwmAUnV2iXSK7umxKFysTwvqK+Ty6ptUcLY/7T3SnCAjUowE4uvwaEej6nXZ7hu/dTtbokKdsPeag==",
|
"integrity": "sha512-QhYP9muVQ0nUO5zztFuPbEwi4+94sJWVjaZds9aMi1l/KNZBiUjdiSUrGHsTaMGDXrYl+r4AS2sUKfgH3w+V3g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -113,9 +113,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-darwin-x64": {
|
"node_modules/@biomejs/cli-darwin-x64": {
|
||||||
"version": "2.5.5",
|
"version": "2.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.3.tgz",
|
||||||
"integrity": "sha512-DamiYc5bUYZ2uxlfc+RLEPtz1Abb6PO5eTbOkufLpSGwd/7AMQAdxhFYiXmwwkJL8IsT8S7GvdgwDHqaMFAvKw==",
|
"integrity": "sha512-NC1Ss13UaW7QZX+y8j44bF7AP0jSJdBl6iRhe0MAkvaSqZy+mWg3GaXsrb+eSoHoGDBtaXWEbMVV0iVN2cZ7cQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -130,9 +130,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-linux-arm64": {
|
"node_modules/@biomejs/cli-linux-arm64": {
|
||||||
"version": "2.5.5",
|
"version": "2.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.3.tgz",
|
||||||
"integrity": "sha512-lRKF/pH/1RiYiBKExi3TCZVAtvzEm77aifrvcNiDFrR9WxeAnDUjDnseb6y2XV85mjitLs6SILGm2XG77cHtSQ==",
|
"integrity": "sha512-ksx1KWeyYW18ILL04msF/J4ZBtBDN33znYK8Z/aNv/vlBVxL9/g3mGP+omgHJKy4+KWbK87vcmmpmurfNjSgiA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -150,9 +150,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-linux-arm64-musl": {
|
"node_modules/@biomejs/cli-linux-arm64-musl": {
|
||||||
"version": "2.5.5",
|
"version": "2.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.3.tgz",
|
||||||
"integrity": "sha512-U4WMl/sy/E/Q73vf15VspakLRRs2LDFcCeBxJnQfXzssb88zpV6PJPaQ3ezhQ7H6Ht2/8bvuZeHgJWzmoxllZg==",
|
"integrity": "sha512-fccix0w6xp6csCXgxeC0dU/3ecgRQal0y+cv2SP9ajNlhe7Yrk2Ug7UDe2j9AT9ZDYitkXpvUKgZjjuoYeP4Vg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -170,9 +170,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-linux-x64": {
|
"node_modules/@biomejs/cli-linux-x64": {
|
||||||
"version": "2.5.5",
|
"version": "2.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.3.tgz",
|
||||||
"integrity": "sha512-H/O39nJEw/2Zm/fm7hrmxxoF8kK/aU1uCoPp70ruXVbomaAdLpJJnCmL11Q2JotT8QVHH06So04Oq53lCSwSwQ==",
|
"integrity": "sha512-yMkJtilsgvILDcVkh187aVLTb64xYsrxYajx5kym+r1ULkO5HUOfu9AYKLGQbOVLwJtT2utNw7hhFNg+17mUYA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -190,9 +190,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-linux-x64-musl": {
|
"node_modules/@biomejs/cli-linux-x64-musl": {
|
||||||
"version": "2.5.5",
|
"version": "2.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.3.tgz",
|
||||||
"integrity": "sha512-m7wC7tjX5Lrmo69dc4md8FeKpPU1NTCY1v7xUoQQ2vadWwNnBS0KZOG8471otFPHrTHihQJAjQPgMObpLvDe6A==",
|
"integrity": "sha512-O/yU9YKRUiHhmcjF2f38PSjseVk3G4VLWYc0G2HWpzdBVREV6G8IGWIVEFf7MFPfWIzNUIvPsEjeAZQIOgnLcQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -210,9 +210,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-win32-arm64": {
|
"node_modules/@biomejs/cli-win32-arm64": {
|
||||||
"version": "2.5.5",
|
"version": "2.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.3.tgz",
|
||||||
"integrity": "sha512-7BryINPuYypLUAH3o/o5ZdgomJ4zn3EDR0ChZJst7n32S6ZhKbgHXuYydLu+YAnx59ehGFR0z/MG6qnzQi3Yyw==",
|
"integrity": "sha512-cX5z+GYwRcqEok0AH3KSfQGgqYd0Nomfp6Fbe1uiTtELE38hdH2k842wQ9wLNaF/JJ7r4rjJQ4VR+ce+fRmQbw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -227,9 +227,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-win32-x64": {
|
"node_modules/@biomejs/cli-win32-x64": {
|
||||||
"version": "2.5.5",
|
"version": "2.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.3.tgz",
|
||||||
"integrity": "sha512-bIBFo+n6MIxdNcVFy5CrurbKiZQiUciK3bt8+O9I4wjFZNTfXLpi+giq47522eXqW5NBc9ulx7dR1SlZKi2J5g==",
|
"integrity": "sha512-ExSaJWi4/u6+GXCszlSKpWSjKNbDseAYqqkCznsCsZ/4uidZ/BEqsCc5/3ctlq6dfIubdIIRSVLC/PG9xPl70Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -244,22 +244,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@cacheable/memory": {
|
"node_modules/@cacheable/memory": {
|
||||||
"version": "2.2.0",
|
"version": "2.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.9.tgz",
|
||||||
"integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==",
|
"integrity": "sha512-HdMx6DoGywB30vacDbBsITbIX4pgFqj1zsrV58jZBUw3klzkNoXhj7qOqAgledhxG7YZI5rBSJg7Zp8/VG0DuA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cacheable/utils": "^2.5.0",
|
"@cacheable/utils": "^2.4.1",
|
||||||
"@keyv/bigmap": "^1.3.1",
|
"@keyv/bigmap": "^1.3.1",
|
||||||
"hookified": "^1.15.1",
|
"hookified": "^1.15.1",
|
||||||
"keyv": "^5.6.0"
|
"keyv": "^5.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@cacheable/utils": {
|
"node_modules/@cacheable/utils": {
|
||||||
"version": "2.5.0",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.4.1.tgz",
|
||||||
"integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==",
|
"integrity": "sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -315,9 +315,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@csstools/css-syntax-patches-for-csstree": {
|
"node_modules/@csstools/css-syntax-patches-for-csstree": {
|
||||||
"version": "1.1.7",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz",
|
||||||
"integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==",
|
"integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -1131,9 +1131,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@oxc-project/types": {
|
"node_modules/@oxc-project/types": {
|
||||||
"version": "0.139.0",
|
"version": "0.138.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz",
|
||||||
"integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==",
|
"integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -1215,9 +1215,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-android-arm64": {
|
"node_modules/@rolldown/binding-android-arm64": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz",
|
||||||
"integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==",
|
"integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1232,9 +1232,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz",
|
||||||
"integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==",
|
"integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1249,9 +1249,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-x64": {
|
"node_modules/@rolldown/binding-darwin-x64": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz",
|
||||||
"integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==",
|
"integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1266,9 +1266,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz",
|
||||||
"integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==",
|
"integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1283,9 +1283,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz",
|
||||||
"integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==",
|
"integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -1300,9 +1300,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz",
|
||||||
"integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==",
|
"integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1320,9 +1320,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz",
|
||||||
"integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==",
|
"integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1340,9 +1340,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz",
|
||||||
"integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==",
|
"integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -1360,9 +1360,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz",
|
||||||
"integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==",
|
"integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@@ -1380,9 +1380,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz",
|
||||||
"integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==",
|
"integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1400,9 +1400,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz",
|
||||||
"integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==",
|
"integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1420,9 +1420,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz",
|
||||||
"integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==",
|
"integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1437,9 +1437,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz",
|
||||||
"integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==",
|
"integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"wasm32"
|
"wasm32"
|
||||||
],
|
],
|
||||||
@@ -1456,9 +1456,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz",
|
||||||
"integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==",
|
"integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1473,9 +1473,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz",
|
||||||
"integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==",
|
"integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2107,9 +2107,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.11.1",
|
"version": "2.10.43",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz",
|
||||||
"integrity": "sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==",
|
"integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -2140,9 +2140,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.28.7",
|
"version": "4.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz",
|
||||||
"integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==",
|
"integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -2160,9 +2160,9 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.10.44",
|
"baseline-browser-mapping": "^2.10.42",
|
||||||
"caniuse-lite": "^1.0.30001806",
|
"caniuse-lite": "^1.0.30001803",
|
||||||
"electron-to-chromium": "^1.5.393",
|
"electron-to-chromium": "^1.5.389",
|
||||||
"node-releases": "^2.0.51",
|
"node-releases": "^2.0.51",
|
||||||
"update-browserslist-db": "^1.2.3"
|
"update-browserslist-db": "^1.2.3"
|
||||||
},
|
},
|
||||||
@@ -2193,14 +2193,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cacheable": {
|
"node_modules/cacheable": {
|
||||||
"version": "2.5.0",
|
"version": "2.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.5.tgz",
|
||||||
"integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==",
|
"integrity": "sha512-EQfaKe09tl615iNvq/TBRWTFf1AKJNXYQSsMx0Z3EI0nA+pVsVPS8wJhnRlkbdacKPh1d0qVIhwTc2zsQNFEEg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cacheable/memory": "^2.2.0",
|
"@cacheable/memory": "^2.0.8",
|
||||||
"@cacheable/utils": "^2.5.0",
|
"@cacheable/utils": "^2.4.1",
|
||||||
"hookified": "^1.15.0",
|
"hookified": "^1.15.0",
|
||||||
"keyv": "^5.6.0",
|
"keyv": "^5.6.0",
|
||||||
"qified": "^0.10.1"
|
"qified": "^0.10.1"
|
||||||
@@ -2217,9 +2217,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001806",
|
"version": "1.0.30001805",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001805.tgz",
|
||||||
"integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==",
|
"integrity": "sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -2635,9 +2635,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.396",
|
"version": "1.5.389",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.396.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz",
|
||||||
"integrity": "sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==",
|
"integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
@@ -2800,13 +2800,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/file-entry-cache": {
|
"node_modules/file-entry-cache": {
|
||||||
"version": "11.1.5",
|
"version": "11.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.3.tgz",
|
||||||
"integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==",
|
"integrity": "sha512-oMbq0PD6VIiIwMF6LIa7MEwd/l9huKwmqRKXqmrkqIZv8CvRbfowL+L0ryAl8h//HfAS0zS+4SbYoRyAoA6BJA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"flat-cache": "^6.1.23"
|
"flat-cache": "^6.1.22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fill-range": {
|
"node_modules/fill-range": {
|
||||||
@@ -2823,21 +2823,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/flat-cache": {
|
"node_modules/flat-cache": {
|
||||||
"version": "6.1.23",
|
"version": "6.1.22",
|
||||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz",
|
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.22.tgz",
|
||||||
"integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==",
|
"integrity": "sha512-N2dnzVJIphnNsjHcrxGW7DePckJ6haPrSFqpsBUhHYgwtKGVq4JrBGielEGD2fCVnsGm1zlBVZ8wGhkyuetgug==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cacheable": "^2.5.0",
|
"cacheable": "^2.3.4",
|
||||||
"flatted": "^3.4.2",
|
"flatted": "^3.4.2",
|
||||||
"hookified": "^1.15.0"
|
"hookified": "^1.15.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/flatted": {
|
"node_modules/flatted": {
|
||||||
"version": "3.4.3",
|
"version": "3.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
|
||||||
"integrity": "sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==",
|
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
@@ -2964,9 +2964,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/globby": {
|
"node_modules/globby": {
|
||||||
"version": "16.2.2",
|
"version": "16.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/globby/-/globby-16.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/globby/-/globby-16.2.0.tgz",
|
||||||
"integrity": "sha512-NLvV9ubZ6NDsJaOpKPy3cQeJpKi9DcWiyCiFUpJPA0YihRqiE6RWaLUmgNNPr8MgPpLZjnBjSmou7uZBRJv9wA==",
|
"integrity": "sha512-QrJia2qDf5BB/V6HYlDTs0I0lBahyjLzpGQg3KT7FnCdTonAyPy2RtY802m2k4ALx6Dp752f82WsOczEVr3l6Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -3070,15 +3070,29 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
||||||
"integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
|
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/image-size": {
|
||||||
|
"version": "0.5.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
|
||||||
|
"integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"bin": {
|
||||||
|
"image-size": "bin/image-size.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/import-fresh": {
|
"node_modules/import-fresh": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||||
@@ -3304,9 +3318,9 @@
|
|||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/less": {
|
"node_modules/less": {
|
||||||
"version": "4.8.0",
|
"version": "4.6.7",
|
||||||
"resolved": "https://registry.npmjs.org/less/-/less-4.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/less/-/less-4.6.7.tgz",
|
||||||
"integrity": "sha512-7Y7DJBMbsW29UGjOG6NGvxQEx71AaDcrryBwYCMaFwn0kj8FkSueKN9r9WexVOetKEhXh38kL++WJncfkEpS+g==",
|
"integrity": "sha512-o3UxHBPPVY1HtCXx15/z1NlknQiWyafRNbtLEv+6xFaDRI2g2xPKIH43do9dSwt8bGLTsjNSaifa48N3d6odsQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -3322,10 +3336,10 @@
|
|||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"errno": "^0.1.1",
|
"errno": "^0.1.1",
|
||||||
"graceful-fs": "^4.1.2",
|
"graceful-fs": "^4.1.2",
|
||||||
|
"image-size": "~0.5.0",
|
||||||
"make-dir": "^5.1.0",
|
"make-dir": "^5.1.0",
|
||||||
"mime": "^1.4.1",
|
"mime": "^1.4.1",
|
||||||
"needle": "^3.1.0",
|
"needle": "^3.1.0",
|
||||||
"probe-image-size": "^7.2.3",
|
|
||||||
"source-map": "~0.6.0"
|
"source-map": "~0.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3597,14 +3611,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash.merge": {
|
|
||||||
"version": "4.6.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
|
||||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"node_modules/lodash.truncate": {
|
"node_modules/lodash.truncate": {
|
||||||
"version": "4.4.2",
|
"version": "4.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
|
||||||
@@ -3727,9 +3733,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.16",
|
"version": "3.3.12",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||||
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
|
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -3926,9 +3932,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.22",
|
"version": "8.5.16",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
|
||||||
"integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==",
|
"integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -3946,7 +3952,7 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.16",
|
"nanoid": "^3.3.12",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
@@ -4052,73 +4058,6 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/probe-image-size": {
|
|
||||||
"version": "7.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/probe-image-size/-/probe-image-size-7.3.0.tgz",
|
|
||||||
"integrity": "sha512-7CaDeBwiAbh6ohXsvLbAZhO7wzsZAmaevfxe39qvCwRh8LyaZfDlBGGLU1CCTgrTLtCOdwBBhjOrIHaIIimHfQ==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/puzrin"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/nodeca"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"lodash.merge": "^4.6.2",
|
|
||||||
"needle": "^2.5.2",
|
|
||||||
"stream-parser": "~0.3.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/probe-image-size/node_modules/debug": {
|
|
||||||
"version": "3.2.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
|
||||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"ms": "^2.1.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/probe-image-size/node_modules/iconv-lite": {
|
|
||||||
"version": "0.4.24",
|
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
|
||||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"safer-buffer": ">= 2.1.2 < 3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/probe-image-size/node_modules/needle": {
|
|
||||||
"version": "2.9.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/needle/-/needle-2.9.1.tgz",
|
|
||||||
"integrity": "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"debug": "^3.2.6",
|
|
||||||
"iconv-lite": "^0.4.4",
|
|
||||||
"sax": "^1.2.4"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"needle": "bin/needle"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 4.4.x"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/property-information": {
|
"node_modules/property-information": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
|
||||||
@@ -4259,13 +4198,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rolldown": {
|
"node_modules/rolldown": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz",
|
||||||
"integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==",
|
"integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "=0.139.0",
|
"@oxc-project/types": "=0.138.0",
|
||||||
"@rolldown/pluginutils": "^1.0.0"
|
"@rolldown/pluginutils": "^1.0.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -4275,21 +4214,21 @@
|
|||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rolldown/binding-android-arm64": "1.1.5",
|
"@rolldown/binding-android-arm64": "1.1.4",
|
||||||
"@rolldown/binding-darwin-arm64": "1.1.5",
|
"@rolldown/binding-darwin-arm64": "1.1.4",
|
||||||
"@rolldown/binding-darwin-x64": "1.1.5",
|
"@rolldown/binding-darwin-x64": "1.1.4",
|
||||||
"@rolldown/binding-freebsd-x64": "1.1.5",
|
"@rolldown/binding-freebsd-x64": "1.1.4",
|
||||||
"@rolldown/binding-linux-arm-gnueabihf": "1.1.5",
|
"@rolldown/binding-linux-arm-gnueabihf": "1.1.4",
|
||||||
"@rolldown/binding-linux-arm64-gnu": "1.1.5",
|
"@rolldown/binding-linux-arm64-gnu": "1.1.4",
|
||||||
"@rolldown/binding-linux-arm64-musl": "1.1.5",
|
"@rolldown/binding-linux-arm64-musl": "1.1.4",
|
||||||
"@rolldown/binding-linux-ppc64-gnu": "1.1.5",
|
"@rolldown/binding-linux-ppc64-gnu": "1.1.4",
|
||||||
"@rolldown/binding-linux-s390x-gnu": "1.1.5",
|
"@rolldown/binding-linux-s390x-gnu": "1.1.4",
|
||||||
"@rolldown/binding-linux-x64-gnu": "1.1.5",
|
"@rolldown/binding-linux-x64-gnu": "1.1.4",
|
||||||
"@rolldown/binding-linux-x64-musl": "1.1.5",
|
"@rolldown/binding-linux-x64-musl": "1.1.4",
|
||||||
"@rolldown/binding-openharmony-arm64": "1.1.5",
|
"@rolldown/binding-openharmony-arm64": "1.1.4",
|
||||||
"@rolldown/binding-wasm32-wasi": "1.1.5",
|
"@rolldown/binding-wasm32-wasi": "1.1.4",
|
||||||
"@rolldown/binding-win32-arm64-msvc": "1.1.5",
|
"@rolldown/binding-win32-arm64-msvc": "1.1.4",
|
||||||
"@rolldown/binding-win32-x64-msvc": "1.1.5"
|
"@rolldown/binding-win32-x64-msvc": "1.1.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/run-parallel": {
|
"node_modules/run-parallel": {
|
||||||
@@ -4508,36 +4447,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/stream-parser": {
|
|
||||||
"version": "0.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/stream-parser/-/stream-parser-0.3.1.tgz",
|
|
||||||
"integrity": "sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"debug": "2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/stream-parser/node_modules/debug": {
|
|
||||||
"version": "2.6.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
|
||||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"ms": "2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/stream-parser/node_modules/ms": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"node_modules/string-width": {
|
"node_modules/string-width": {
|
||||||
"version": "8.2.1",
|
"version": "8.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz",
|
||||||
@@ -4588,9 +4497,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/stylelint": {
|
"node_modules/stylelint": {
|
||||||
"version": "17.14.1",
|
"version": "17.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.14.0.tgz",
|
||||||
"integrity": "sha512-xVQwyiuxALUBNB2fBe0tmNemg9KqLtdj3T64mioFDar79B2cU8LIyz+3KL6LdiHs9NkeNfwxpKSaIVOY8f112g==",
|
"integrity": "sha512-8xkHPpdqYryeIsOgfsYTmr6cIeC4nLYWk5S8BPxpodq8mIuepggkMljsHewWfuAjj/+qpRKou2QerhjMH3iasg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -4606,7 +4515,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@csstools/css-calc": "^3.2.1",
|
"@csstools/css-calc": "^3.2.1",
|
||||||
"@csstools/css-parser-algorithms": "^4.0.0",
|
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||||
"@csstools/css-syntax-patches-for-csstree": "^1.1.6",
|
"@csstools/css-syntax-patches-for-csstree": "^1.1.5",
|
||||||
"@csstools/css-tokenizer": "^4.0.0",
|
"@csstools/css-tokenizer": "^4.0.0",
|
||||||
"@csstools/media-query-list-parser": "^5.0.0",
|
"@csstools/media-query-list-parser": "^5.0.0",
|
||||||
"@csstools/selector-resolve-nested": "^4.0.0",
|
"@csstools/selector-resolve-nested": "^4.0.0",
|
||||||
@@ -4618,9 +4527,9 @@
|
|||||||
"debug": "^4.4.3",
|
"debug": "^4.4.3",
|
||||||
"fast-glob": "^3.3.3",
|
"fast-glob": "^3.3.3",
|
||||||
"fastest-levenshtein": "^1.0.16",
|
"fastest-levenshtein": "^1.0.16",
|
||||||
"file-entry-cache": "^11.1.5",
|
"file-entry-cache": "^11.1.3",
|
||||||
"global-modules": "^2.0.0",
|
"global-modules": "^2.0.0",
|
||||||
"globby": "^16.2.1",
|
"globby": "^16.2.0",
|
||||||
"globjoin": "^0.1.4",
|
"globjoin": "^0.1.4",
|
||||||
"html-tags": "^5.1.0",
|
"html-tags": "^5.1.0",
|
||||||
"ignore": "^7.0.5",
|
"ignore": "^7.0.5",
|
||||||
@@ -4630,12 +4539,12 @@
|
|||||||
"micromatch": "^4.0.8",
|
"micromatch": "^4.0.8",
|
||||||
"normalize-path": "^3.0.0",
|
"normalize-path": "^3.0.0",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"postcss": "^8.5.16",
|
"postcss": "^8.5.15",
|
||||||
"postcss-safe-parser": "^7.0.1",
|
"postcss-safe-parser": "^7.0.1",
|
||||||
"postcss-selector-parser": "^7.1.4",
|
"postcss-selector-parser": "^7.1.4",
|
||||||
"postcss-value-parser": "^4.2.0",
|
"postcss-value-parser": "^4.2.0",
|
||||||
"string-width": "^8.2.1",
|
"string-width": "^8.2.1",
|
||||||
"supports-hyperlinks": "^4.5.0",
|
"supports-hyperlinks": "^4.4.0",
|
||||||
"svg-tags": "^1.0.0",
|
"svg-tags": "^1.0.0",
|
||||||
"table": "^6.9.0",
|
"table": "^6.9.0",
|
||||||
"write-file-atomic": "^7.0.1"
|
"write-file-atomic": "^7.0.1"
|
||||||
@@ -4796,9 +4705,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/supports-hyperlinks": {
|
"node_modules/supports-hyperlinks": {
|
||||||
"version": "4.5.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-4.4.0.tgz",
|
||||||
"integrity": "sha512-ZW2OvfeCXrNTbLakPUzjQG922EeGCOteFSVoek5DKStTh898wf7zgtuFlzQN8HfZCxC3Eh02yJVrRW51hADf+w==",
|
"integrity": "sha512-UKbpT93hN5Nr9go5UY7bopIB9YQlMz9nm/ct4IXt/irb5YRkn9WaqrOBJGZ5Pwvsd5FQzSVeYlGdXoCAPQZrPg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -5094,16 +5003,16 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "8.1.5",
|
"version": "8.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz",
|
||||||
"integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==",
|
"integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lightningcss": "^1.32.0",
|
"lightningcss": "^1.32.0",
|
||||||
"picomatch": "^4.0.5",
|
"picomatch": "^4.0.5",
|
||||||
"postcss": "^8.5.17",
|
"postcss": "^8.5.16",
|
||||||
"rolldown": "~1.1.5",
|
"rolldown": "~1.1.4",
|
||||||
"tinyglobby": "^0.2.17"
|
"tinyglobby": "^0.2.17"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -5172,9 +5081,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite-bundle-analyzer": {
|
"node_modules/vite-bundle-analyzer": {
|
||||||
"version": "1.3.9",
|
"version": "1.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/vite-bundle-analyzer/-/vite-bundle-analyzer-1.3.9.tgz",
|
"resolved": "https://registry.npmjs.org/vite-bundle-analyzer/-/vite-bundle-analyzer-1.3.8.tgz",
|
||||||
"integrity": "sha512-hhy975B+ToseJaSJp3mURHriZUrMgaM2qx0BkP62kN6Yp46rw7EE3dl8ExFWYdn00OIwe7GbsA5PknvstNWG4Q==",
|
"integrity": "sha512-IIk7WPhoYs7pyo75jwI+dFt7yykgjK7NY+dqnJtiZnyqP2k6NgPb3TY80FLFjtgnfk/o+OjI18+anKyeviCbRA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
@@ -29,21 +29,21 @@
|
|||||||
"swiped-events": "1.2.0"
|
"swiped-events": "1.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.5.5",
|
"@biomejs/biome": "2.5.3",
|
||||||
"@types/node": "^26.1.1",
|
"@types/node": "^26.1.1",
|
||||||
"browserslist": "^4.28.7",
|
"browserslist": "^4.28.6",
|
||||||
"browserslist-to-esbuild": "^2.1.1",
|
"browserslist-to-esbuild": "^2.1.1",
|
||||||
"edge.js": "^6.5.1",
|
"edge.js": "^6.5.1",
|
||||||
"less": "^4.8.0",
|
"less": "^4.6.7",
|
||||||
"mathjs": "^15.2.0",
|
"mathjs": "^15.2.0",
|
||||||
"sharp": "~0.35.3",
|
"sharp": "~0.35.3",
|
||||||
"sort-package-json": "^4.0.0",
|
"sort-package-json": "^4.0.0",
|
||||||
"stylelint": "^17.14.1",
|
"stylelint": "^17.14.0",
|
||||||
"stylelint-config-standard-less": "^4.1.0",
|
"stylelint-config-standard-less": "^4.1.0",
|
||||||
"stylelint-prettier": "^5.0.3",
|
"stylelint-prettier": "^5.0.3",
|
||||||
"svgo": "^4.0.2",
|
"svgo": "^4.0.2",
|
||||||
"typescript": "~7.0.2",
|
"typescript": "~7.0.2",
|
||||||
"vite": "^8.1.5",
|
"vite": "^8.1.4",
|
||||||
"vite-bundle-analyzer": "^1.3.9"
|
"vite-bundle-analyzer": "^1.3.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,13 @@ import { assertElement } from "../util/assertElement.ts";
|
|||||||
|
|
||||||
const fetchResults = async (qInput: HTMLInputElement, query: string): Promise<void> => {
|
const fetchResults = async (qInput: HTMLInputElement, query: string): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const res = await http("GET", `./autocompleter?q=${query}`);
|
let res: Response;
|
||||||
|
|
||||||
|
if (settings.method === "GET") {
|
||||||
|
res = await http("GET", `./autocompleter?q=${query}`);
|
||||||
|
} else {
|
||||||
|
res = await http("POST", "./autocompleter", { body: new URLSearchParams({ q: query }) });
|
||||||
|
}
|
||||||
|
|
||||||
const results = await res.json();
|
const results = await res.json();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
import { Plugin } from "../Plugin.ts";
|
||||||
|
import { settings } from "../toolkit.ts";
|
||||||
|
import { assertElement } from "../util/assertElement.ts";
|
||||||
|
|
||||||
|
type Message = { role: "user" | "assistant"; content: string };
|
||||||
|
type ContextItem = { title: string; url: string; snippet: string };
|
||||||
|
type StreamLine = { delta?: string; done?: boolean; model?: string; error?: string };
|
||||||
|
|
||||||
|
// keep in sync with the server side defaults (searx/ai_summary.py)
|
||||||
|
const MAX_CONTEXT_ITEMS = 5;
|
||||||
|
const MAX_HISTORY_MESSAGES = 12;
|
||||||
|
const MAX_TITLE_LENGTH = 300;
|
||||||
|
const MAX_SNIPPET_LENGTH = 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fills the AI summary placeholder (rendered by the ai_summary plugin of the
|
||||||
|
* server) by streaming an answer from the /ai_summary endpoint, with an
|
||||||
|
* expand button and a follow-up question chat.
|
||||||
|
*/
|
||||||
|
export default class AiSummary extends Plugin {
|
||||||
|
private readonly messages: Message[] = [];
|
||||||
|
private context: ContextItem[] = [];
|
||||||
|
private controller: AbortController | undefined;
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
|
super("ai_summary");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async run(): Promise<void> {
|
||||||
|
const box = document.getElementById("ai_summary");
|
||||||
|
if (!box) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { query } = box.dataset;
|
||||||
|
if (!query) return;
|
||||||
|
|
||||||
|
if (box.dataset.grounding === "1") {
|
||||||
|
this.context = AiSummary.collectContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.wireControls(box);
|
||||||
|
|
||||||
|
this.messages.push({ role: "user", content: query });
|
||||||
|
await this.exchange(box);
|
||||||
|
} catch (error) {
|
||||||
|
// never fail silently, always leave a message in the summary box
|
||||||
|
AiSummary.showError(box, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async post(): Promise<void> {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scrape the top search results from the DOM as grounding context.
|
||||||
|
*/
|
||||||
|
private static collectContext(): ContextItem[] {
|
||||||
|
const items: ContextItem[] = [];
|
||||||
|
for (const article of document.querySelectorAll<HTMLElement>("#urls article.result")) {
|
||||||
|
if (items.length >= MAX_CONTEXT_ITEMS) break;
|
||||||
|
|
||||||
|
const link = article.querySelector<HTMLAnchorElement>("h3 a");
|
||||||
|
if (!link) continue;
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
title: (link.textContent ?? "").trim().slice(0, MAX_TITLE_LENGTH),
|
||||||
|
url: link.href,
|
||||||
|
snippet: (article.querySelector(".content")?.textContent ?? "").trim().slice(0, MAX_SNIPPET_LENGTH)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
private wireControls(box: HTMLElement): void {
|
||||||
|
const body = box.querySelector<HTMLElement>(".ai-summary-body");
|
||||||
|
const moreButton = box.querySelector<HTMLElement>(".ai-summary-more");
|
||||||
|
const followupForm = box.querySelector<HTMLFormElement>(".ai-summary-followup");
|
||||||
|
assertElement(body);
|
||||||
|
assertElement(moreButton);
|
||||||
|
assertElement(followupForm);
|
||||||
|
|
||||||
|
moreButton.addEventListener("click", () => {
|
||||||
|
const collapsed = body.classList.toggle("collapsed");
|
||||||
|
moreButton.textContent = collapsed
|
||||||
|
? (moreButton.dataset.btnTextCollapsed ?? "")
|
||||||
|
: (moreButton.dataset.btnTextNotCollapsed ?? "");
|
||||||
|
followupForm.classList.toggle("invisible", collapsed);
|
||||||
|
});
|
||||||
|
|
||||||
|
followupForm.addEventListener("submit", (event: Event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const input = followupForm.querySelector<HTMLInputElement>("input");
|
||||||
|
assertElement(input);
|
||||||
|
|
||||||
|
const question = input.value.trim();
|
||||||
|
if (!question || this.controller) return;
|
||||||
|
|
||||||
|
input.value = "";
|
||||||
|
this.messages.push({ role: "user", content: question });
|
||||||
|
// the server rejects too long histories, drop the oldest messages
|
||||||
|
this.messages.splice(0, this.messages.length - (MAX_HISTORY_MESSAGES - 1));
|
||||||
|
|
||||||
|
const questionElement = Object.assign(document.createElement("p"), {
|
||||||
|
textContent: question,
|
||||||
|
className: "ai-summary-question"
|
||||||
|
});
|
||||||
|
box.querySelector(".ai-summary-answers")?.append(questionElement);
|
||||||
|
|
||||||
|
void this.exchange(box);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the message history to /ai_summary and stream the answer into a new
|
||||||
|
* block in the answer area.
|
||||||
|
*/
|
||||||
|
private async exchange(box: HTMLElement): Promise<void> {
|
||||||
|
const answers = box.querySelector<HTMLElement>(".ai-summary-answers");
|
||||||
|
assertElement(answers);
|
||||||
|
|
||||||
|
const block = Object.assign(document.createElement("p"), {
|
||||||
|
className: "ai-summary-content typing"
|
||||||
|
});
|
||||||
|
answers.append(block);
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
this.controller = controller;
|
||||||
|
|
||||||
|
let text = "";
|
||||||
|
const handleLine = (line: string): void => {
|
||||||
|
if (!line.trim()) return;
|
||||||
|
|
||||||
|
const data = JSON.parse(line) as StreamLine;
|
||||||
|
if (data.error) {
|
||||||
|
throw new Error(data.error);
|
||||||
|
}
|
||||||
|
if (data.delta) {
|
||||||
|
text += data.delta;
|
||||||
|
block.textContent = text;
|
||||||
|
this.updateMoreButton(box);
|
||||||
|
}
|
||||||
|
if (data.done && data.model) {
|
||||||
|
const modelElement = box.querySelector<HTMLElement>(".ai-summary-model");
|
||||||
|
if (modelElement) modelElement.textContent = data.model;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("./ai_summary", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ messages: this.messages, context: this.context }),
|
||||||
|
signal: controller.signal
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
// the status is shown to the user: it is often the only thing
|
||||||
|
// available to diagnose a failure on a device without dev tools
|
||||||
|
throw new Error(`HTTP ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.body) {
|
||||||
|
const reader = res.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = "";
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
// biome-ignore lint/performance/noAwaitInLoops: chunks of a stream are read sequentially
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
const lines = buffer.split("\n");
|
||||||
|
buffer = lines.pop() ?? "";
|
||||||
|
for (const line of lines) handleLine(line);
|
||||||
|
}
|
||||||
|
handleLine(buffer);
|
||||||
|
} else {
|
||||||
|
// some webviews (e.g. Brave iOS) wrap fetch and don't expose a
|
||||||
|
// streaming body: no typing effect, render the answer in one go
|
||||||
|
const body = await res.text();
|
||||||
|
for (const line of body.split("\n")) handleLine(line);
|
||||||
|
}
|
||||||
|
this.messages.push({ role: "assistant", content: text });
|
||||||
|
} catch (error) {
|
||||||
|
AiSummary.showError(box, error);
|
||||||
|
} finally {
|
||||||
|
block.classList.remove("typing");
|
||||||
|
this.controller = undefined;
|
||||||
|
this.updateMoreButton(box);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static showError(box: HTMLElement, error: unknown): void {
|
||||||
|
console.error("Error loading AI summary:", error);
|
||||||
|
|
||||||
|
const message = settings.translations?.error_loading_ai_summary ?? "Error loading the AI summary";
|
||||||
|
const reason = error instanceof Error && error.message ? ` (${error.message})` : "";
|
||||||
|
|
||||||
|
const errorElement = Object.assign(document.createElement("div"), {
|
||||||
|
textContent: `${message}${reason}`,
|
||||||
|
className: "dialog-error"
|
||||||
|
});
|
||||||
|
errorElement.setAttribute("role", "alert");
|
||||||
|
(box.querySelector<HTMLElement>(".ai-summary-answers") ?? box).append(errorElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the expand button as soon as the (collapsed) body overflows.
|
||||||
|
*/
|
||||||
|
private updateMoreButton(box: HTMLElement): void {
|
||||||
|
const body = box.querySelector<HTMLElement>(".ai-summary-body");
|
||||||
|
const moreButton = box.querySelector<HTMLElement>(".ai-summary-more");
|
||||||
|
if (!(body && moreButton)) return;
|
||||||
|
|
||||||
|
if (!body.classList.contains("collapsed") || body.scrollHeight > body.clientHeight + 4) {
|
||||||
|
moreButton.classList.remove("invisible");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,6 +34,13 @@ ready(() => {
|
|||||||
where: [Endpoints.results]
|
where: [Endpoints.results]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (settings.plugins?.includes("ai_summary")) {
|
||||||
|
load(() => import("./plugin/AiSummary.ts").then(({ default: Plugin }) => new Plugin()), {
|
||||||
|
on: "endpoint",
|
||||||
|
where: [Endpoints.results]
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ready(
|
ready(
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
@keyframes ai-summary-caret {
|
||||||
|
50% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-summary {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
.ai-summary-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
.ai-summary-title {
|
||||||
|
font-size: 0.9em;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-summary-model {
|
||||||
|
font-size: 0.8em;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-summary-body.collapsed {
|
||||||
|
max-height: 8em;
|
||||||
|
overflow: hidden;
|
||||||
|
mask-image: linear-gradient(to bottom, #000 60%, transparent 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-summary-answers {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-summary-content {
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-line;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
|
||||||
|
&.typing::after {
|
||||||
|
content: "▍";
|
||||||
|
animation: ai-summary-caret 1s step-end infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-summary-question {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-summary-more {
|
||||||
|
align-self: center;
|
||||||
|
border: none;
|
||||||
|
.show-content-button();
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-summary-followup {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border: 1px solid var(--color-sidebar-border);
|
||||||
|
background: var(--color-answer-background);
|
||||||
|
color: var(--color-answer-font);
|
||||||
|
.rounded-corners-tiny;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border: none;
|
||||||
|
.show-content-button();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-summary-disclaimer {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.8em;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,7 +35,8 @@ table {
|
|||||||
width: 300px;
|
width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"] {
|
input[type="text"],
|
||||||
|
input[type="password"] {
|
||||||
width: 13.25rem;
|
width: 13.25rem;
|
||||||
color: var(--color-toolkit-input-text-font);
|
color: var(--color-toolkit-input-text-font);
|
||||||
border: none;
|
border: none;
|
||||||
@@ -65,7 +66,8 @@ table {
|
|||||||
width: 15em;
|
width: 15em;
|
||||||
|
|
||||||
select,
|
select,
|
||||||
input[type="text"] {
|
input[type="text"],
|
||||||
|
input[type="password"] {
|
||||||
font-size: inherit !important;
|
font-size: inherit !important;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
.ltr-margin-right(1rem);
|
.ltr-margin-right(1rem);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
@import "stats.less";
|
@import "stats.less";
|
||||||
@import "result_templates.less";
|
@import "result_templates.less";
|
||||||
@import "weather.less";
|
@import "weather.less";
|
||||||
|
@import "ai_summary.less";
|
||||||
|
|
||||||
// for index.html template
|
// for index.html template
|
||||||
@import "index.less";
|
@import "index.less";
|
||||||
|
|||||||
@@ -29,11 +29,10 @@ By default and without any extensions, SearXNG serves these resolvers:
|
|||||||
- ``duckduckgo``
|
- ``duckduckgo``
|
||||||
- ``allesedv``
|
- ``allesedv``
|
||||||
- ``google``
|
- ``google``
|
||||||
- ``kagi``
|
|
||||||
- ``yandex``
|
- ``yandex``
|
||||||
|
|
||||||
With the above setting favicons are displayed, the user has the option to
|
With the above setting favicons are displayed, the user has the option to
|
||||||
deactivate this feature in their settings. If the user is to have the option of
|
deactivate this feature in his settings. If the user is to have the option of
|
||||||
selecting from several *resolvers*, a further setting is required / but this
|
selecting from several *resolvers*, a further setting is required / but this
|
||||||
setting will be discussed :ref:`later <register resolvers>` in this article,
|
setting will be discussed :ref:`later <register resolvers>` in this article,
|
||||||
first we have to setup the favicons cache.
|
first we have to setup the favicons cache.
|
||||||
@@ -209,7 +208,6 @@ choose from, the following configuration could be used:
|
|||||||
"duckduckgo" = "searx.favicons.resolvers.duckduckgo"
|
"duckduckgo" = "searx.favicons.resolvers.duckduckgo"
|
||||||
"allesedv" = "searx.favicons.resolvers.allesedv"
|
"allesedv" = "searx.favicons.resolvers.allesedv"
|
||||||
# "google" = "searx.favicons.resolvers.google"
|
# "google" = "searx.favicons.resolvers.google"
|
||||||
# "kagi" = "searx.favicons.resolvers.kagi"
|
|
||||||
# "yandex" = "searx.favicons.resolvers.yandex"
|
# "yandex" = "searx.favicons.resolvers.yandex"
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
@@ -228,7 +226,6 @@ into the *proxy*:
|
|||||||
- :py:obj:`searx.favicons.resolvers.duckduckgo`
|
- :py:obj:`searx.favicons.resolvers.duckduckgo`
|
||||||
- :py:obj:`searx.favicons.resolvers.allesedv`
|
- :py:obj:`searx.favicons.resolvers.allesedv`
|
||||||
- :py:obj:`searx.favicons.resolvers.google`
|
- :py:obj:`searx.favicons.resolvers.google`
|
||||||
- :py:obj:`searx.favicons.resolvers.kagi`
|
|
||||||
- :py:obj:`searx.favicons.resolvers.yandex`
|
- :py:obj:`searx.favicons.resolvers.yandex`
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -25,3 +25,4 @@ Settings
|
|||||||
settings_outgoing
|
settings_outgoing
|
||||||
settings_categories_as_tabs
|
settings_categories_as_tabs
|
||||||
settings_plugins
|
settings_plugins
|
||||||
|
settings_ai_summary
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
.. _settings ai_summary:
|
||||||
|
|
||||||
|
===============
|
||||||
|
``ai_summary:``
|
||||||
|
===============
|
||||||
|
|
||||||
|
.. sidebar:: Further reading ..
|
||||||
|
|
||||||
|
- :ref:`ai_summary plugin`
|
||||||
|
- :ref:`settings plugins`
|
||||||
|
- :ref:`settings preferences`
|
||||||
|
|
||||||
|
Configuration of the :ref:`AI Summary plugin <ai_summary plugin>`, which shows a
|
||||||
|
short AI generated answer above the search results.
|
||||||
|
|
||||||
|
The text is produced by an LLM server that you run; SearXNG ships no model and
|
||||||
|
contacts no AI provider of its own. Any server implementing the `OpenAI chat
|
||||||
|
completions API`_ works: `Ollama`_, vLLM, llama.cpp, LM Studio, Hugging Face
|
||||||
|
TGI and others. The plugin is not activated by default.
|
||||||
|
|
||||||
|
.. _ai_summary quickstart:
|
||||||
|
|
||||||
|
Quickstart
|
||||||
|
==========
|
||||||
|
|
||||||
|
A local setup on the same machine as SearXNG, in four steps.
|
||||||
|
|
||||||
|
**1. Install Ollama**
|
||||||
|
|
||||||
|
.. code:: sh
|
||||||
|
|
||||||
|
curl -fsSL https://ollama.com/install.sh | sh
|
||||||
|
|
||||||
|
**2. Download a model**
|
||||||
|
|
||||||
|
.. code:: sh
|
||||||
|
|
||||||
|
ollama pull gemma3:4b
|
||||||
|
|
||||||
|
``gemma3:4b`` needs roughly 4 GB of memory and runs on CPU if you have no GPU.
|
||||||
|
On a smaller machine use ``gemma3:1b``; any model in the `Ollama library`_
|
||||||
|
works.
|
||||||
|
|
||||||
|
**3. Configure SearXNG**
|
||||||
|
|
||||||
|
Add this to your ``settings.yml``:
|
||||||
|
|
||||||
|
.. code:: yaml
|
||||||
|
|
||||||
|
ai_summary:
|
||||||
|
base_url: "http://127.0.0.1:11434"
|
||||||
|
model: "gemma3:4b"
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
searx.plugins.ai_summary.SXNGPlugin:
|
||||||
|
active: true
|
||||||
|
# keep the plugins you already had, see the warning below
|
||||||
|
searx.plugins.calculator.SXNGPlugin: {active: true}
|
||||||
|
searx.plugins.hash_plugin.SXNGPlugin: {active: true}
|
||||||
|
searx.plugins.self_info.SXNGPlugin: {active: true}
|
||||||
|
searx.plugins.unit_converter.SXNGPlugin: {active: true}
|
||||||
|
searx.plugins.ahmia_filter.SXNGPlugin: {active: true}
|
||||||
|
searx.plugins.hostnames.SXNGPlugin: {active: true}
|
||||||
|
searx.plugins.time_zone.SXNGPlugin: {active: true}
|
||||||
|
searx.plugins.tracker_url_remover.SXNGPlugin: {active: true}
|
||||||
|
searx.plugins.infinite_scroll.SXNGPlugin: {active: false}
|
||||||
|
searx.plugins.oa_doi_rewrite.SXNGPlugin: {active: false}
|
||||||
|
searx.plugins.tor_check.SXNGPlugin: {active: false}
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
A ``plugins:`` block **replaces** the default list, it is not merged into it
|
||||||
|
(:ref:`settings plugins`). Listing only the AI Summary plugin switches
|
||||||
|
every other plugin off, which is why the block above repeats the defaults --
|
||||||
|
drop the lines for plugins you do not want.
|
||||||
|
|
||||||
|
**4. Restart SearXNG** and search for something.
|
||||||
|
|
||||||
|
The summary appears above the results while it is still being written.
|
||||||
|
|
||||||
|
Options
|
||||||
|
=======
|
||||||
|
|
||||||
|
Only ``base_url`` is required. A model is needed too, but if ``model`` is left
|
||||||
|
empty the first entry of ``models`` is used.
|
||||||
|
|
||||||
|
.. code:: yaml
|
||||||
|
|
||||||
|
ai_summary:
|
||||||
|
base_url: "http://127.0.0.1:11434"
|
||||||
|
model: "gemma3:4b"
|
||||||
|
grounding: true
|
||||||
|
|
||||||
|
.. autoclass:: searx.ai_summary.SettingsAISummary
|
||||||
|
:members:
|
||||||
|
|
||||||
|
Servers that require authentication
|
||||||
|
===================================
|
||||||
|
|
||||||
|
vLLM and llama.cpp started with ``--api-key``, a gateway such as LiteLLM, or an
|
||||||
|
LLM server behind an authenticating reverse proxy all expect a key. Set it
|
||||||
|
with ``api_key``:
|
||||||
|
|
||||||
|
.. code:: yaml
|
||||||
|
|
||||||
|
ai_summary:
|
||||||
|
base_url: "http://127.0.0.1:8000"
|
||||||
|
api_key: "sk-..."
|
||||||
|
model: "gemma3:4b"
|
||||||
|
|
||||||
|
The key is sent as an ``Authorization: Bearer`` header and only to the server in
|
||||||
|
``base_url``. Users who configure a server of their own in the
|
||||||
|
``ai_summary_server`` preference never receive it; they set their own key in the
|
||||||
|
``ai_summary_api_key`` preference.
|
||||||
|
|
||||||
|
SearXNG has no separate secret store, so the key is held in ``settings.yml`` --
|
||||||
|
that file should be readable only by the user SearXNG runs as.
|
||||||
|
|
||||||
|
.. _ai_summary grounding:
|
||||||
|
|
||||||
|
Grounding
|
||||||
|
=========
|
||||||
|
|
||||||
|
With ``grounding`` enabled, which is the default, the query **and the top search
|
||||||
|
results** (title, URL and snippet, at most ``max_context_items`` of them) are
|
||||||
|
sent to the LLM server, and the answer reflects what the search found. With it
|
||||||
|
disabled only the query is sent and the model answers from its training data.
|
||||||
|
Users can change this in the ``ai_summary_grounding`` preference.
|
||||||
|
|
||||||
|
What leaves the network therefore depends on where the LLM server runs: with a
|
||||||
|
server on localhost or in the local network, nothing does.
|
||||||
|
|
||||||
|
Public SearXNG instances
|
||||||
|
========================
|
||||||
|
|
||||||
|
The server URL, model and API key are user preferences so that someone running
|
||||||
|
SearXNG at home can switch models or debug their LLM server from the
|
||||||
|
preferences page, without editing ``settings.yml`` and restarting.
|
||||||
|
|
||||||
|
On public SearXNG instances, those same preferences let any visitor choose the
|
||||||
|
address the summary request is sent to. The request is made by the SearXNG
|
||||||
|
host, so a visitor can use it to reach machines on your network that they have
|
||||||
|
no route to themselves -- an `SSRF`_ vector.
|
||||||
|
|
||||||
|
.. attention::
|
||||||
|
|
||||||
|
Lock ``ai_summary_server`` on any SearXNG instance that is reachable by
|
||||||
|
people outside your household.
|
||||||
|
|
||||||
|
Locking a preference makes SearXNG use your configured value and ignore the
|
||||||
|
user's (:ref:`settings preferences`):
|
||||||
|
|
||||||
|
.. code:: yaml
|
||||||
|
|
||||||
|
preferences:
|
||||||
|
lock:
|
||||||
|
- ai_summary_server
|
||||||
|
- ai_summary_api_key
|
||||||
|
- ai_summary_model
|
||||||
|
- ai_summary_grounding
|
||||||
|
|
||||||
|
``ai_summary_server`` is the one that matters: locking it closes the SSRF
|
||||||
|
vector. Locking ``ai_summary_api_key`` additionally stops visitors making your
|
||||||
|
SearXNG instance send an ``Authorization`` header of their choosing to a host of
|
||||||
|
their choosing. ``ai_summary_model`` and ``ai_summary_grounding`` are about
|
||||||
|
cost and consistency rather than security.
|
||||||
|
|
||||||
|
.. _Ollama: https://ollama.com/
|
||||||
|
.. _Ollama library: https://ollama.com/library
|
||||||
|
.. _OpenAI chat completions API: https://platform.openai.com/docs/api-reference/chat
|
||||||
|
.. _SSRF: https://owasp.org/www-community/attacks/Server_Side_Request_Forgery
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
search:
|
search:
|
||||||
safe_search: 0
|
safe_search: 0
|
||||||
autocomplete: "duckduckgo"
|
autocomplete: ""
|
||||||
favicon_resolver: ""
|
favicon_resolver: ""
|
||||||
default_lang: ""
|
default_lang: ""
|
||||||
ban_time_on_fail: 5
|
ban_time_on_fail: 5
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
- ``2``: Strict
|
- ``2``: Strict
|
||||||
|
|
||||||
``autocomplete``:
|
``autocomplete``:
|
||||||
Existing autocomplete backends, set blank to turn it off.
|
Existing autocomplete backends, leave blank to turn it off.
|
||||||
|
|
||||||
- ``360search``
|
- ``360search``
|
||||||
- ``baidu``
|
- ``baidu``
|
||||||
@@ -41,7 +41,6 @@
|
|||||||
- ``dbpedia``
|
- ``dbpedia``
|
||||||
- ``duckduckgo``
|
- ``duckduckgo``
|
||||||
- ``google``
|
- ``google``
|
||||||
- ``kagi``
|
|
||||||
- ``mwmbl``
|
- ``mwmbl``
|
||||||
- ``naver``
|
- ``naver``
|
||||||
- ``privacywall``
|
- ``privacywall``
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
limiter: false
|
limiter: false
|
||||||
public_instance: false
|
public_instance: false
|
||||||
image_proxy: false
|
image_proxy: false
|
||||||
method: "GET"
|
method: "POST"
|
||||||
default_http_headers:
|
default_http_headers:
|
||||||
X-Content-Type-Options : nosniff
|
X-Content-Type-Options : nosniff
|
||||||
X-Download-Options : noopen
|
X-Download-Options : noopen
|
||||||
@@ -58,8 +58,8 @@
|
|||||||
|
|
||||||
``method`` : ``GET`` | ``POST``
|
``method`` : ``GET`` | ``POST``
|
||||||
|
|
||||||
HTTP method. By default, ``GET`` is used / The ``POST`` method has the
|
HTTP method. By defaults ``POST`` is used / The ``POST`` method has the
|
||||||
advantage with some browsers that the history is not saved, but
|
advantage with some WEB browsers that the history is not easy to read, but
|
||||||
there are also various disadvantages that sometimes **severely restrict the
|
there are also various disadvantages that sometimes **severely restrict the
|
||||||
ease of use for the end user** (e.g. back button to jump back to the previous
|
ease of use for the end user** (e.g. back button to jump back to the previous
|
||||||
search page and drag & drop of search term to new tabs do not work as
|
search page and drag & drop of search term to new tabs do not work as
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
.. _exaapi engine:
|
|
||||||
|
|
||||||
==============
|
|
||||||
Exa API Engine
|
|
||||||
==============
|
|
||||||
|
|
||||||
.. automodule:: searx.engines.exaapi
|
|
||||||
:members:
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
.. _jina engine:
|
|
||||||
|
|
||||||
===========
|
|
||||||
Jina Engine
|
|
||||||
===========
|
|
||||||
|
|
||||||
.. automodule:: searx.engines.jina
|
|
||||||
:members:
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.. _engine presearch:
|
||||||
|
|
||||||
|
================
|
||||||
|
Presearch Engine
|
||||||
|
================
|
||||||
|
|
||||||
|
.. automodule:: searx.engines.presearch
|
||||||
|
:members:
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
.. _yandex api engine:
|
|
||||||
|
|
||||||
=================
|
|
||||||
Yandex Search API
|
|
||||||
=================
|
|
||||||
|
|
||||||
.. automodule:: searx.engines.yandex_api
|
|
||||||
:members:
|
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
.. _ai_summary plugin:
|
||||||
|
|
||||||
|
==========
|
||||||
|
AI Summary
|
||||||
|
==========
|
||||||
|
|
||||||
|
.. sidebar:: Further reading ..
|
||||||
|
|
||||||
|
- :ref:`Configuration <settings ai_summary>`
|
||||||
|
- :ref:`dev plugin`
|
||||||
|
- :ref:`result types`
|
||||||
|
|
||||||
|
The AI Summary plugin shows a generated answer above the ordinary search
|
||||||
|
results. The text comes from an LLM server run by the administrator, which
|
||||||
|
speaks the `OpenAI chat completions API`_ -- `Ollama`_, Hugging Face TGI,
|
||||||
|
LiteLLM, vLLM, llama.cpp and anything else implementing that specification.
|
||||||
|
See :ref:`its configuration <settings ai_summary>` for how to set one up.
|
||||||
|
|
||||||
|
The summary is generated asynchronously: the result page is delivered without
|
||||||
|
delay and carries an empty placeholder, which the browser fills from a second,
|
||||||
|
streaming request.
|
||||||
|
|
||||||
|
Request flow
|
||||||
|
============
|
||||||
|
|
||||||
|
.. _ai_summary dataflow:
|
||||||
|
|
||||||
|
.. kernel-render:: DOT
|
||||||
|
:alt: Data flow between browser, SearXNG and the LLM server
|
||||||
|
:caption: A search that produces a summary: two requests, not one
|
||||||
|
|
||||||
|
digraph ai_summary {
|
||||||
|
rankdir=LR;
|
||||||
|
graph [fontname="sans-serif", ranksep=1.1, nodesep=0.4];
|
||||||
|
node [fontname="sans-serif", fontsize=11, shape=box, style="rounded,filled",
|
||||||
|
fillcolor="#f4f4f4", color="#999999"];
|
||||||
|
edge [fontname="sans-serif", fontsize=9, color="#666666"];
|
||||||
|
|
||||||
|
browser [label="browser"];
|
||||||
|
searxng [label="SearXNG"];
|
||||||
|
engines [label="search engines", fillcolor="#ffffff"];
|
||||||
|
llm [label="LLM server\nOllama, vLLM, ...", fillcolor="#ffffff"];
|
||||||
|
|
||||||
|
browser -> searxng [label=" 1 GET /search"];
|
||||||
|
searxng -> engines [label=" 2 query"];
|
||||||
|
searxng -> browser [label=" 3 result page,\l empty summary box\l", constraint=false];
|
||||||
|
browser -> searxng [label=" 4 POST /ai_summary\l (query + results)\l"];
|
||||||
|
searxng -> llm [label=" 5 POST /v1/chat/completions"];
|
||||||
|
llm -> searxng [label=" 6 SSE token stream", constraint=false];
|
||||||
|
searxng -> browser [label=" 7 NDJSON token stream", constraint=false];
|
||||||
|
}
|
||||||
|
|
||||||
|
Steps 1 to 3 are an ordinary SearXNG search. :py:obj:`SXNGPlugin.post_search
|
||||||
|
<searx.plugins.ai_summary.SXNGPlugin.post_search>` adds an empty
|
||||||
|
:py:obj:`searx.result_types.AiSummary` placeholder to the answer area and
|
||||||
|
returns; the result page is not delayed.
|
||||||
|
|
||||||
|
Steps 4 to 7 run in the browser once the page is rendered.
|
||||||
|
``client/simple/src/js/plugin/AiSummary.ts`` posts to the ``/ai_summary``
|
||||||
|
endpoint (registered in :py:obj:`searx.webapp`), which opens a streaming
|
||||||
|
request to the LLM server and re-emits the tokens as they arrive.
|
||||||
|
|
||||||
|
The two streams use different formats. The LLM server sends `SSE`_ --
|
||||||
|
``data: {...}`` lines terminated by ``data: [DONE]``. SearXNG re-emits them to
|
||||||
|
the browser as `NDJSON`_: one JSON object per line, ``{"delta": "..."}`` for
|
||||||
|
each chunk of text and a final ``{"done": true}``.
|
||||||
|
|
||||||
|
When no summary is generated
|
||||||
|
============================
|
||||||
|
|
||||||
|
:py:obj:`SXNGPlugin.post_search
|
||||||
|
<searx.plugins.ai_summary.SXNGPlugin.post_search>` adds no placeholder for:
|
||||||
|
|
||||||
|
- page two and beyond
|
||||||
|
- categories other than *general*
|
||||||
|
- non-HTML output formats (the JSON, CSV and RSS APIs)
|
||||||
|
- queries an engine already answered with an infobox (wikipedia, wikidata) or
|
||||||
|
an instant answer (e.g. ddg definitions)
|
||||||
|
- an empty query, or no LLM server configured
|
||||||
|
|
||||||
|
API keys
|
||||||
|
========
|
||||||
|
|
||||||
|
The administrator's ``api_key`` is sent only to the configured ``base_url``.
|
||||||
|
Users who point the ``ai_summary_server`` preference at a server of their own
|
||||||
|
authenticate it with their own ``ai_summary_api_key`` preference, which is
|
||||||
|
stored in a cookie and excluded from the preferences URL; the administrator's
|
||||||
|
key is never sent to such a server. A server URL carrying credentials in its
|
||||||
|
userinfo is ignored, and the administrator's default is used instead.
|
||||||
|
|
||||||
|
:py:obj:`_server_api_key <searx.plugins.ai_summary._server_api_key>` implements
|
||||||
|
the rule.
|
||||||
|
|
||||||
|
Reference
|
||||||
|
=========
|
||||||
|
|
||||||
|
.. automodule:: searx.plugins.ai_summary
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. _Ollama: https://ollama.com/
|
||||||
|
.. _OpenAI chat completions API: https://platform.openai.com/docs/api-reference/chat
|
||||||
|
.. _SSE: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
|
||||||
|
.. _NDJSON: https://github.com/ndjson/ndjson-spec
|
||||||
@@ -7,6 +7,7 @@ Built-in Plugins
|
|||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 1
|
:maxdepth: 1
|
||||||
|
|
||||||
|
ai_summary
|
||||||
calculator
|
calculator
|
||||||
hash_plugin
|
hash_plugin
|
||||||
hostnames
|
hostnames
|
||||||
|
|||||||
+5
-1
@@ -20,11 +20,15 @@ If you don't trust anyone, you can set up your own, see :ref:`installation`.
|
|||||||
|
|
||||||
- :ref:`self hosted <installation>`
|
- :ref:`self hosted <installation>`
|
||||||
- :ref:`no user tracking / no profiling <SearXNG protect privacy>`
|
- :ref:`no user tracking / no profiling <SearXNG protect privacy>`
|
||||||
- javascript & cookies are optional
|
- script & cookies are optional
|
||||||
|
- secure, encrypted connections
|
||||||
- :ref:`{{engines | length}} search engines <configured engines>`
|
- :ref:`{{engines | length}} search engines <configured engines>`
|
||||||
- `58 translations <https://translate.codeberg.org/projects/searxng/searxng/>`_
|
- `58 translations <https://translate.codeberg.org/projects/searxng/searxng/>`_
|
||||||
- about 70 `well maintained <https://uptime.searxng.org/>`__ instances on searx.space_
|
- about 70 `well maintained <https://uptime.searxng.org/>`__ instances on searx.space_
|
||||||
- :ref:`easy integration of search engines <demo online engine>`
|
- :ref:`easy integration of search engines <demo online engine>`
|
||||||
|
- professional development: `CI <https://github.com/searxng/searxng/actions>`_,
|
||||||
|
`quality assurance <https://dev.searxng.org/>`_ &
|
||||||
|
`automated tested UI <https://dev.searxng.org/screenshots.html>`_
|
||||||
|
|
||||||
.. sidebar:: be a part
|
.. sidebar:: be a part
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
certifi==2026.7.22
|
certifi==2026.6.17
|
||||||
babel==2.18.0
|
babel==2.18.0
|
||||||
flask-babel==4.0.0
|
flask-babel==4.0.0
|
||||||
flask==3.1.3
|
flask==3.1.3
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ class SettingsPref(msgspec.Struct, kw_only=True, forbid_unknown_fields=True):
|
|||||||
"theme",
|
"theme",
|
||||||
"results_on_new_tab",
|
"results_on_new_tab",
|
||||||
"doi_resolver",
|
"doi_resolver",
|
||||||
|
"ai_summary_server",
|
||||||
|
"ai_summary_model",
|
||||||
|
"ai_summary_grounding",
|
||||||
"simple_style",
|
"simple_style",
|
||||||
"center_alignment",
|
"center_alignment",
|
||||||
"query_in_title",
|
"query_in_title",
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
"""Implementations needed for the AI Summary plugin
|
||||||
|
(:py:obj:`searx.plugins.ai_summary`)."""
|
||||||
|
# pylint: disable=too-few-public-methods
|
||||||
|
|
||||||
|
# Struct fields aren't discovered in Python 3.14
|
||||||
|
# - https://github.com/searxng/searxng/issues/5284
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
__all__ = ["SettingsAISummary", "MODELS", "model_choices", "build_chat_messages"]
|
||||||
|
|
||||||
|
import msgspec
|
||||||
|
|
||||||
|
DEFAULT_SYSTEM_PROMPT = (
|
||||||
|
"You are a search assistant. Answer the user's search query concisely in"
|
||||||
|
" a few short paragraphs of plain text. If you are unsure or don't know"
|
||||||
|
" the answer, say so."
|
||||||
|
)
|
||||||
|
|
||||||
|
DEFAULT_SYSTEM_PROMPT_GROUNDED = (
|
||||||
|
"You are a search assistant. Answer the user's search query concisely in"
|
||||||
|
" a few short paragraphs of plain text, using the following search results"
|
||||||
|
" as context when they are relevant. If you are unsure or don't know the"
|
||||||
|
" answer, say so.\n\nSearch results:\n\n{context}"
|
||||||
|
)
|
||||||
|
|
||||||
|
MODELS: list[str] = []
|
||||||
|
"""List of model names a user can select from. Populated once at
|
||||||
|
application setup by :py:obj:`searx.plugins.ai_summary.SXNGPlugin.init`."""
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsAISummary(msgspec.Struct, kw_only=True, forbid_unknown_fields=True):
|
||||||
|
"""Options for configuring the AI Summary plugin.
|
||||||
|
|
||||||
|
.. code:: yaml
|
||||||
|
|
||||||
|
ai_summary:
|
||||||
|
base_url: "http://127.0.0.1:11434"
|
||||||
|
model: "llama3.2:3b"
|
||||||
|
models:
|
||||||
|
- "llama3.2:3b"
|
||||||
|
- "gemma3:4b"
|
||||||
|
"""
|
||||||
|
|
||||||
|
base_url: str = ""
|
||||||
|
"""Default base URL of the LLM server (e.g. ``http://127.0.0.1:11434``
|
||||||
|
for Ollama). Any server that implements the OpenAI chat completions API
|
||||||
|
works (Ollama, vLLM, llama.cpp, LM Studio, Hugging Face TGI, ...). Users
|
||||||
|
can set their own server URL in the preferences (``ai_summary_server``)
|
||||||
|
unless that preference is locked."""
|
||||||
|
|
||||||
|
api_key: str = ""
|
||||||
|
"""Optional API key of the LLM server in
|
||||||
|
:py:obj:`SettingsAISummary.base_url`, sent in an ``Authorization: Bearer``
|
||||||
|
header. Needed by servers that require authentication, e.g. vLLM or
|
||||||
|
llama.cpp started with ``--api-key``, or an LLM server behind an
|
||||||
|
authenticating reverse proxy.
|
||||||
|
|
||||||
|
This key is **only** sent to :py:obj:`SettingsAISummary.base_url`: a user
|
||||||
|
who points the ``ai_summary_server`` preference at a server of their own
|
||||||
|
gets no ``Authorization`` header from it, so the key can't be captured by
|
||||||
|
a third party. For their own server, users configure their own key in the
|
||||||
|
``ai_summary_api_key`` preference (see
|
||||||
|
:py:obj:`searx.plugins.ai_summary._server_api_key`)."""
|
||||||
|
|
||||||
|
model: str = ""
|
||||||
|
"""Name of the default model (e.g. ``llama3.2:3b``). If empty, the first
|
||||||
|
entry of :py:obj:`SettingsAISummary.models` is used. Users can set their
|
||||||
|
own model in the preferences (``ai_summary_model``) unless that preference
|
||||||
|
is locked."""
|
||||||
|
|
||||||
|
models: list[str] = []
|
||||||
|
"""List of model names suggested to the user in the preferences. If empty
|
||||||
|
and :py:obj:`SettingsAISummary.base_url` is set, the list is requested
|
||||||
|
once at application setup from the LLM server (``GET /v1/models``)."""
|
||||||
|
|
||||||
|
grounding: bool = True
|
||||||
|
"""Default of the ``ai_summary_grounding`` user preference: ground the
|
||||||
|
summary on the search results. Grounded summaries are more accurate and
|
||||||
|
more current at a moderate extra cost (the search results are sent along
|
||||||
|
with the query, so the prompt is longer). Users can still opt in/out in
|
||||||
|
the preferences unless that preference is locked."""
|
||||||
|
|
||||||
|
connect_timeout: float = 5.0
|
||||||
|
"""Timeout (seconds) to establish a TCP connection to the LLM server."""
|
||||||
|
|
||||||
|
read_timeout: float = 30.0
|
||||||
|
"""Maximum gap (seconds) between two chunks of the token stream."""
|
||||||
|
|
||||||
|
stream_timeout: float = 120.0
|
||||||
|
"""Wall clock limit (seconds) for one completion."""
|
||||||
|
|
||||||
|
max_context_items: int = 5
|
||||||
|
"""Maximum number of search results accepted as grounding context."""
|
||||||
|
|
||||||
|
max_history_messages: int = 12
|
||||||
|
"""Maximum number of messages (follow-up chat history) per request."""
|
||||||
|
|
||||||
|
max_message_length: int = 4000
|
||||||
|
"""Maximum length (characters) of a single message or context snippet."""
|
||||||
|
|
||||||
|
system_prompt: str = DEFAULT_SYSTEM_PROMPT
|
||||||
|
"""System prompt used when the *grounding* preference is off."""
|
||||||
|
|
||||||
|
system_prompt_grounded: str = DEFAULT_SYSTEM_PROMPT_GROUNDED
|
||||||
|
"""System prompt used when the *grounding* preference is on. The
|
||||||
|
placeholder ``{context}`` is replaced by an enumeration of the search
|
||||||
|
results sent along with the query."""
|
||||||
|
|
||||||
|
|
||||||
|
def model_choices() -> list[str]:
|
||||||
|
"""Model names a user can select from in the preferences."""
|
||||||
|
return list(MODELS)
|
||||||
|
|
||||||
|
|
||||||
|
def build_chat_messages(
|
||||||
|
cfg: SettingsAISummary,
|
||||||
|
messages: list[dict[str, str]],
|
||||||
|
context: list[dict[str, str]] | None = None,
|
||||||
|
) -> list[dict[str, str]]:
|
||||||
|
"""Build the message list for the chat completions request from the
|
||||||
|
(already validated) request ``messages``, prepending a system prompt. When
|
||||||
|
``context`` items are given, the grounded system prompt is used and the
|
||||||
|
context items are serialized into its ``{context}`` placeholder."""
|
||||||
|
|
||||||
|
if context:
|
||||||
|
ctx_lines = [
|
||||||
|
f"[{no}] {item.get('title', '')} — {item.get('snippet', '')} ({item.get('url', '')})"
|
||||||
|
for no, item in enumerate(context[: cfg.max_context_items], start=1)
|
||||||
|
]
|
||||||
|
system_prompt = cfg.system_prompt_grounded.replace("{context}", "\n".join(ctx_lines))
|
||||||
|
else:
|
||||||
|
system_prompt = cfg.system_prompt
|
||||||
|
|
||||||
|
return [{"role": "system", "content": system_prompt}, *messages]
|
||||||
@@ -16,7 +16,7 @@ from . import Answerer, AnswererInfo
|
|||||||
|
|
||||||
def random_characters():
|
def random_characters():
|
||||||
random_string_letters = string.ascii_lowercase + string.digits + string.ascii_uppercase
|
random_string_letters = string.ascii_lowercase + string.digits + string.ascii_uppercase
|
||||||
return random.choices(random_string_letters, k=random.randint(8, 32))
|
return [random.choice(random_string_letters) for _ in range(random.randint(8, 32))]
|
||||||
|
|
||||||
|
|
||||||
def random_string():
|
def random_string():
|
||||||
|
|||||||
+1
-20
@@ -62,7 +62,7 @@ def bing(query: str, _sxng_locale: str) -> list[str]:
|
|||||||
# bing search autocompleter
|
# bing search autocompleter
|
||||||
base_url = "https://www.bing.com/AS/Suggestions?"
|
base_url = "https://www.bing.com/AS/Suggestions?"
|
||||||
# cvid has to be a 32 character long string consisting of numbers and uppsercase characters
|
# cvid has to be a 32 character long string consisting of numbers and uppsercase characters
|
||||||
cvid = ''.join(random.choices(string.ascii_uppercase + string.digits, k=32))
|
cvid = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(32))
|
||||||
response = get(base_url + urlencode({'qry': query, 'csr': 1, 'cvid': cvid}))
|
response = get(base_url + urlencode({'qry': query, 'csr': 1, 'cvid': cvid}))
|
||||||
results: list[str] = []
|
results: list[str] = []
|
||||||
|
|
||||||
@@ -157,24 +157,6 @@ def google_complete(query: str, sxng_locale: str) -> list[str]:
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
def kagi(query: str, sxng_locale: str) -> list[str]:
|
|
||||||
"""Autocomplete from Kagi."""
|
|
||||||
|
|
||||||
args: dict[str, str] = {'q': query}
|
|
||||||
|
|
||||||
if '-' in sxng_locale:
|
|
||||||
args['r'] = sxng_locale.split('-')[1].lower()
|
|
||||||
|
|
||||||
resp = get("https://kagisuggest.com/api/autosuggest?" + urlencode(args))
|
|
||||||
results: list[str] = []
|
|
||||||
|
|
||||||
if resp.ok:
|
|
||||||
data = resp.json()
|
|
||||||
if len(data) > 1:
|
|
||||||
results = data[1]
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def mwmbl(query: str, _sxng_locale: str) -> list[str]:
|
def mwmbl(query: str, _sxng_locale: str) -> list[str]:
|
||||||
"""Autocomplete from Mwmbl_."""
|
"""Autocomplete from Mwmbl_."""
|
||||||
|
|
||||||
@@ -398,7 +380,6 @@ backends: dict[str, t.Callable[[str, str], list[str]]] = {
|
|||||||
'dbpedia': dbpedia,
|
'dbpedia': dbpedia,
|
||||||
'duckduckgo': duckduckgo,
|
'duckduckgo': duckduckgo,
|
||||||
'google': google_complete,
|
'google': google_complete,
|
||||||
'kagi': kagi,
|
|
||||||
'mwmbl': mwmbl,
|
'mwmbl': mwmbl,
|
||||||
'naver': naver,
|
'naver': naver,
|
||||||
'privacywall': privacywall,
|
'privacywall': privacywall,
|
||||||
|
|||||||
@@ -151,6 +151,6 @@ def get_token() -> str:
|
|||||||
if token:
|
if token:
|
||||||
token = token.decode('UTF-8') # type: ignore
|
token = token.decode('UTF-8') # type: ignore
|
||||||
else:
|
else:
|
||||||
token = ''.join(random.choices(string.ascii_lowercase + string.digits, k=16))
|
token = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(16))
|
||||||
valkey_client.set(TOKEN_KEY, token, ex=TOKEN_LIVE_TIME)
|
valkey_client.set(TOKEN_KEY, token, ex=TOKEN_LIVE_TIME)
|
||||||
return token
|
return token
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -288,7 +288,7 @@
|
|||||||
"oc": "Kwanza",
|
"oc": "Kwanza",
|
||||||
"pa": "ਅੰਗੋਲਨ ਕਵਾਂਜ਼ਾ",
|
"pa": "ਅੰਗੋਲਨ ਕਵਾਂਜ਼ਾ",
|
||||||
"pl": "Kwanza",
|
"pl": "Kwanza",
|
||||||
"pt": "kwanza",
|
"pt": "Kwanza",
|
||||||
"ru": "ангольская кванза",
|
"ru": "ангольская кванза",
|
||||||
"si": "ක්වන්සා",
|
"si": "ක්වන්සා",
|
||||||
"sr": "анголска кванза",
|
"sr": "анголска кванза",
|
||||||
@@ -334,7 +334,6 @@
|
|||||||
"ro": "Peso argentinian",
|
"ro": "Peso argentinian",
|
||||||
"ru": "аргентинское песо",
|
"ru": "аргентинское песо",
|
||||||
"sk": "Argentinské peso",
|
"sk": "Argentinské peso",
|
||||||
"sl": "argentinski peso",
|
|
||||||
"sr": "аргентински пезос",
|
"sr": "аргентински пезос",
|
||||||
"sv": "Argentinsk peso",
|
"sv": "Argentinsk peso",
|
||||||
"ta": "ஆர்ஜென்டின பீசோ",
|
"ta": "ஆர்ஜென்டின பீசோ",
|
||||||
@@ -2003,7 +2002,7 @@
|
|||||||
"eo": "ganaa cedio",
|
"eo": "ganaa cedio",
|
||||||
"es": "cedi",
|
"es": "cedi",
|
||||||
"fi": "Cedi",
|
"fi": "Cedi",
|
||||||
"fr": "cedi",
|
"fr": "Cedi",
|
||||||
"ga": "cedi",
|
"ga": "cedi",
|
||||||
"gl": "Cedi",
|
"gl": "Cedi",
|
||||||
"he": "סדי גאני",
|
"he": "סדי גאני",
|
||||||
@@ -2953,7 +2952,7 @@
|
|||||||
"pap": "won nortkoreano",
|
"pap": "won nortkoreano",
|
||||||
"pl": "won północnokoreański",
|
"pl": "won północnokoreański",
|
||||||
"pt": "won norte-coreano",
|
"pt": "won norte-coreano",
|
||||||
"ro": "won nord-coreean",
|
"ro": "Won nord-coreean",
|
||||||
"ru": "вона КНДР",
|
"ru": "вона КНДР",
|
||||||
"sk": "severokorejsky won",
|
"sk": "severokorejsky won",
|
||||||
"sl": "severnokorejski von",
|
"sl": "severnokorejski von",
|
||||||
@@ -3095,7 +3094,6 @@
|
|||||||
"ca": "tenge",
|
"ca": "tenge",
|
||||||
"cs": "Tenge",
|
"cs": "Tenge",
|
||||||
"cy": "tenge Casachstan",
|
"cy": "tenge Casachstan",
|
||||||
"da": "Tenge",
|
|
||||||
"de": "Tenge",
|
"de": "Tenge",
|
||||||
"en": "Kazakhstani tenge",
|
"en": "Kazakhstani tenge",
|
||||||
"eo": "kazaĥa tengo",
|
"eo": "kazaĥa tengo",
|
||||||
@@ -4836,7 +4834,6 @@
|
|||||||
"nl": "Seychelse roepie",
|
"nl": "Seychelse roepie",
|
||||||
"pl": "Rupia seszelska",
|
"pl": "Rupia seszelska",
|
||||||
"pt": "rupia das Seicheles",
|
"pt": "rupia das Seicheles",
|
||||||
"ro": "rupie seychelloză",
|
|
||||||
"ru": "сейшельская рупия",
|
"ru": "сейшельская рупия",
|
||||||
"sk": "Seychelská rupia",
|
"sk": "Seychelská rupia",
|
||||||
"sl": "sejšelska rupija",
|
"sl": "sejšelska rupija",
|
||||||
@@ -5067,7 +5064,6 @@
|
|||||||
"nl": "Somalische shilling",
|
"nl": "Somalische shilling",
|
||||||
"pl": "Szyling somalijski",
|
"pl": "Szyling somalijski",
|
||||||
"pt": "xelim somaliano",
|
"pt": "xelim somaliano",
|
||||||
"ro": "șiling somalez",
|
|
||||||
"ru": "сомалийский шиллинг",
|
"ru": "сомалийский шиллинг",
|
||||||
"sk": "Somálsky šiling",
|
"sk": "Somálsky šiling",
|
||||||
"sl": "somalski šiling",
|
"sl": "somalski šiling",
|
||||||
@@ -5887,7 +5883,6 @@
|
|||||||
"ja": "ドン",
|
"ja": "ドン",
|
||||||
"ko": "베트남 동",
|
"ko": "베트남 동",
|
||||||
"lt": "Vietnamo dongas",
|
"lt": "Vietnamo dongas",
|
||||||
"ms": "Dồng Vietnam",
|
|
||||||
"nl": "Vietnamese dong",
|
"nl": "Vietnamese dong",
|
||||||
"oc": "Dong",
|
"oc": "Dong",
|
||||||
"pa": "ਵੀਅਤਨਾਮੀ ਦੋਙ",
|
"pa": "ਵੀਅਤਨਾਮੀ ਦੋਙ",
|
||||||
@@ -6127,8 +6122,7 @@
|
|||||||
"ro": "Gulden caraibian",
|
"ro": "Gulden caraibian",
|
||||||
"ru": "Карибский гульден",
|
"ru": "Карибский гульден",
|
||||||
"sk": "Karibský gulden",
|
"sk": "Karibský gulden",
|
||||||
"sl": "karibski goldinar",
|
"sl": "karibski goldinar"
|
||||||
"sv": "Karibisk gulden"
|
|
||||||
},
|
},
|
||||||
"XDR": {
|
"XDR": {
|
||||||
"ar": "حقوق السحب الخاصة",
|
"ar": "حقوق السحب الخاصة",
|
||||||
@@ -6730,8 +6724,6 @@
|
|||||||
"antilliaanse gulden": "ANG",
|
"antilliaanse gulden": "ANG",
|
||||||
"antilski gulden": "ANG",
|
"antilski gulden": "ANG",
|
||||||
"aoa": "AOA",
|
"aoa": "AOA",
|
||||||
"apvienotās karalistes ekonomika": "GBP",
|
|
||||||
"apvienotās karalistes saimniecība": "GBP",
|
|
||||||
"apvienotās karalistes sterliņu mārciņa": "GBP",
|
"apvienotās karalistes sterliņu mārciņa": "GBP",
|
||||||
"ar": "MGA",
|
"ar": "MGA",
|
||||||
"arab accounting dinar": "XAD",
|
"arab accounting dinar": "XAD",
|
||||||
@@ -6844,7 +6836,6 @@
|
|||||||
"avustralya doları": "AUD",
|
"avustralya doları": "AUD",
|
||||||
"awg": "AWG",
|
"awg": "AWG",
|
||||||
"az arany mint befektetés": "XAU",
|
"az arany mint befektetés": "XAU",
|
||||||
"az egyesült királyság gazdasága": "GBP",
|
|
||||||
"azerbaidžanin manat": "AZN",
|
"azerbaidžanin manat": "AZN",
|
||||||
"azerbaidžano manatas": "AZN",
|
"azerbaidžano manatas": "AZN",
|
||||||
"azerbaidžānas manats": "AZN",
|
"azerbaidžānas manats": "AZN",
|
||||||
@@ -7028,7 +7019,6 @@
|
|||||||
"bir etíope": "ETB",
|
"bir etíope": "ETB",
|
||||||
"biras": "ETB",
|
"biras": "ETB",
|
||||||
"birleşik arap emirlikleri dirhemi": "AED",
|
"birleşik arap emirlikleri dirhemi": "AED",
|
||||||
"birleşik krallık ekonomisi": "GBP",
|
|
||||||
"birma kjato": "MMK",
|
"birma kjato": "MMK",
|
||||||
"birr": "ETB",
|
"birr": "ETB",
|
||||||
"birr da etiópia": "ETB",
|
"birr da etiópia": "ETB",
|
||||||
@@ -7116,19 +7106,15 @@
|
|||||||
"brit font": "GBP",
|
"brit font": "GBP",
|
||||||
"brita pundo": "GBP",
|
"brita pundo": "GBP",
|
||||||
"britaj pundoj": "GBP",
|
"britaj pundoj": "GBP",
|
||||||
"britannian talous": "GBP",
|
|
||||||
"britanska funta": "GBP",
|
"britanska funta": "GBP",
|
||||||
"britanski funt": "GBP",
|
"britanski funt": "GBP",
|
||||||
"britische wirtschaft": "GBP",
|
|
||||||
"britisches pfund": "GBP",
|
"britisches pfund": "GBP",
|
||||||
"british economy": "GBP",
|
|
||||||
"british pound": "GBP",
|
"british pound": "GBP",
|
||||||
"britisk pund": "GBP",
|
"britisk pund": "GBP",
|
||||||
"britiske pund": "GBP",
|
"britiske pund": "GBP",
|
||||||
"brits pond": "GBP",
|
"brits pond": "GBP",
|
||||||
"britse pond": "GBP",
|
"britse pond": "GBP",
|
||||||
"britská libra": "GBP",
|
"britská libra": "GBP",
|
||||||
"brittisk ekonomi": "GBP",
|
|
||||||
"brittiska pund": "GBP",
|
"brittiska pund": "GBP",
|
||||||
"brittiskt pund": "GBP",
|
"brittiskt pund": "GBP",
|
||||||
"brunei doları": "BND",
|
"brunei doları": "BND",
|
||||||
@@ -7212,7 +7198,6 @@
|
|||||||
"cedi du ghana": "GHS",
|
"cedi du ghana": "GHS",
|
||||||
"cedi ghana": "GHS",
|
"cedi ghana": "GHS",
|
||||||
"cedi ghanese": "GHS",
|
"cedi ghanese": "GHS",
|
||||||
"cedi ghanéen": "GHS",
|
|
||||||
"centr afrika franko": "XAF",
|
"centr afrika franko": "XAF",
|
||||||
"central african cfa franc": "XAF",
|
"central african cfa franc": "XAF",
|
||||||
"centralafrikansk cfa franc": "XAF",
|
"centralafrikansk cfa franc": "XAF",
|
||||||
@@ -7315,6 +7300,7 @@
|
|||||||
"colón costa ricense": "CRC",
|
"colón costa ricense": "CRC",
|
||||||
"colón costa riquenho": "CRC",
|
"colón costa riquenho": "CRC",
|
||||||
"colón costa riquense": "CRC",
|
"colón costa riquense": "CRC",
|
||||||
|
"colón costa riqueny": "CRC",
|
||||||
"colón costaricain": "CRC",
|
"colón costaricain": "CRC",
|
||||||
"colón costaricano": "CRC",
|
"colón costaricano": "CRC",
|
||||||
"colón costaricien": "CRC",
|
"colón costaricien": "CRC",
|
||||||
@@ -8427,7 +8413,6 @@
|
|||||||
"dólares canadenses": "CAD",
|
"dólares canadenses": "CAD",
|
||||||
"dólares estadounidenses": "USD",
|
"dólares estadounidenses": "USD",
|
||||||
"dólares neozelandeses": "NZD",
|
"dólares neozelandeses": "NZD",
|
||||||
"dồng vietnam": "VND",
|
|
||||||
"dram": "AMD",
|
"dram": "AMD",
|
||||||
"dram armean": "AMD",
|
"dram armean": "AMD",
|
||||||
"dram armenia": "AMD",
|
"dram armenia": "AMD",
|
||||||
@@ -8451,7 +8436,6 @@
|
|||||||
"droits de tirage speciaux": "XDR",
|
"droits de tirage speciaux": "XDR",
|
||||||
"droits de tirage spéciaux": "XDR",
|
"droits de tirage spéciaux": "XDR",
|
||||||
"dschibuti franc": "DJF",
|
"dschibuti franc": "DJF",
|
||||||
"dvn": "VND",
|
|
||||||
"dzd": "DZD",
|
"dzd": "DZD",
|
||||||
"dzsibuti frank": "DJF",
|
"dzsibuti frank": "DJF",
|
||||||
"džibučio frankas": "DJF",
|
"džibučio frankas": "DJF",
|
||||||
@@ -8465,21 +8449,6 @@
|
|||||||
"eastern caribbean currency union": "XCD",
|
"eastern caribbean currency union": "XCD",
|
||||||
"eastern caribbean dollar": "XCD",
|
"eastern caribbean dollar": "XCD",
|
||||||
"ec$": "XCD",
|
"ec$": "XCD",
|
||||||
"economi'r deyrnas unedig": "GBP",
|
|
||||||
"economia": "GBP",
|
|
||||||
"economia del regne unit": "GBP",
|
|
||||||
"economia del regno unito": "GBP",
|
|
||||||
"economia del reialme unit": "GBP",
|
|
||||||
"economia del reino unido": "GBP",
|
|
||||||
"economia do reino unido": "GBP",
|
|
||||||
"economia regatului unit": "GBP",
|
|
||||||
"economie du royaume uni": "GBP",
|
|
||||||
"economie van het verenigd koninkrijk": "GBP",
|
|
||||||
"economía del reino unido": "GBP",
|
|
||||||
"economía do reino unido": "GBP",
|
|
||||||
"economy": "GBP",
|
|
||||||
"economy of the uk": "GBP",
|
|
||||||
"economy of the united kingdom": "GBP",
|
|
||||||
"egipatska funta": "EGP",
|
"egipatska funta": "EGP",
|
||||||
"egipta pundo": "EGP",
|
"egipta pundo": "EGP",
|
||||||
"egipto svaras": "EGP",
|
"egipto svaras": "EGP",
|
||||||
@@ -8499,12 +8468,6 @@
|
|||||||
"einr": "INR",
|
"einr": "INR",
|
||||||
"eiro": "EUR",
|
"eiro": "EUR",
|
||||||
"ekialdeko karibeko dolar": "XCD",
|
"ekialdeko karibeko dolar": "XCD",
|
||||||
"ekonomi britania raya": "GBP",
|
|
||||||
"ekonomi united kingdom": "GBP",
|
|
||||||
"ekonomie van die verenigde koninkryk": "GBP",
|
|
||||||
"ekonomika spojeného království": "GBP",
|
|
||||||
"ekonomika v spojenom kráľovstve": "GBP",
|
|
||||||
"ekonomio de britujo": "GBP",
|
|
||||||
"el peso": "GTQ",
|
"el peso": "GTQ",
|
||||||
"emalangeni": "SZL",
|
"emalangeni": "SZL",
|
||||||
"emas sebagai pelaburan": "XAU",
|
"emas sebagai pelaburan": "XAU",
|
||||||
@@ -8536,7 +8499,6 @@
|
|||||||
"ermenistan dramı": "AMD",
|
"ermenistan dramı": "AMD",
|
||||||
"ern": "ERN",
|
"ern": "ERN",
|
||||||
"erreal brasildar": "BRL",
|
"erreal brasildar": "BRL",
|
||||||
"erresuma batuko ekonomia": "GBP",
|
|
||||||
"errublo": "RUB",
|
"errublo": "RUB",
|
||||||
"errublo errusiar": "RUB",
|
"errublo errusiar": "RUB",
|
||||||
"errupia indiar": "INR",
|
"errupia indiar": "INR",
|
||||||
@@ -8607,8 +8569,6 @@
|
|||||||
"eyrir": "ISK",
|
"eyrir": "ISK",
|
||||||
"e£": "EGP",
|
"e£": "EGP",
|
||||||
"èuro": "EUR",
|
"èuro": "EUR",
|
||||||
"économie britannique": "GBP",
|
|
||||||
"économie du royaume uni": "GBP",
|
|
||||||
"észak ír font": "GBP",
|
"észak ír font": "GBP",
|
||||||
"észak koreai von": "KPW",
|
"észak koreai von": "KPW",
|
||||||
"e₹": "INR",
|
"e₹": "INR",
|
||||||
@@ -8742,9 +8702,6 @@
|
|||||||
"forintti": "HUF",
|
"forintti": "HUF",
|
||||||
"forinți": "HUF",
|
"forinți": "HUF",
|
||||||
"fòrint": "HUF",
|
"fòrint": "HUF",
|
||||||
"förenade konungariket storbritannien och irlands ekonomi": "GBP",
|
|
||||||
"förenade konungariket storbritannien och nordirlands ekonomi": "GBP",
|
|
||||||
"förenade kungarikets ekonomi": "GBP",
|
|
||||||
"franak cfp": "XPF",
|
"franak cfp": "XPF",
|
||||||
"franc": [
|
"franc": [
|
||||||
"XPF",
|
"XPF",
|
||||||
@@ -8997,9 +8954,6 @@
|
|||||||
"gold als kapitalanlage": "XAU",
|
"gold als kapitalanlage": "XAU",
|
||||||
"gold as an investment": "XAU",
|
"gold as an investment": "XAU",
|
||||||
"gold as currency": "XAU",
|
"gold as currency": "XAU",
|
||||||
"gospodarka wielkiej brytanii": "GBP",
|
|
||||||
"gospodarstvo ujedinjenog kraljevstva": "GBP",
|
|
||||||
"gospodarstvo združenega kraljestva": "GBP",
|
|
||||||
"gourde": "HTG",
|
"gourde": "HTG",
|
||||||
"gourde haiti": "HTG",
|
"gourde haiti": "HTG",
|
||||||
"gourde haitiano": "HTG",
|
"gourde haitiano": "HTG",
|
||||||
@@ -9419,7 +9373,6 @@
|
|||||||
"juaņs": "CNY",
|
"juaņs": "CNY",
|
||||||
"juhokoréjsky won": "KRW",
|
"juhokoréjsky won": "KRW",
|
||||||
"juhosudánska libra": "SSP",
|
"juhosudánska libra": "SSP",
|
||||||
"jungtinės karalystės ekonomika": "GBP",
|
|
||||||
"jungtinių arabų emyratų dirhamas": "AED",
|
"jungtinių arabų emyratų dirhamas": "AED",
|
||||||
"jungtinių valstijų doleris": "USD",
|
"jungtinių valstijų doleris": "USD",
|
||||||
"južnoafrički rand": "ZAR",
|
"južnoafrički rand": "ZAR",
|
||||||
@@ -9484,7 +9437,6 @@
|
|||||||
"karibi forint": "XCG",
|
"karibi forint": "XCG",
|
||||||
"karibia guldeno": "XCG",
|
"karibia guldeno": "XCG",
|
||||||
"karibischer gulden": "XCG",
|
"karibischer gulden": "XCG",
|
||||||
"karibisk gulden": "XCG",
|
|
||||||
"karibski goldinar": "XCG",
|
"karibski goldinar": "XCG",
|
||||||
"karibský gulden": "XCG",
|
"karibský gulden": "XCG",
|
||||||
"karipski gulden": "XCG",
|
"karipski gulden": "XCG",
|
||||||
@@ -9545,9 +9497,6 @@
|
|||||||
"kina papua nugini": "PGK",
|
"kina papua nugini": "PGK",
|
||||||
"kina papuana": "PGK",
|
"kina papuana": "PGK",
|
||||||
"kina papuásia": "PGK",
|
"kina papuásia": "PGK",
|
||||||
"kinh tế anh": "GBP",
|
|
||||||
"kinh tế vương quốc anh": "GBP",
|
|
||||||
"kinh tế vương quốc liên hiệp anh và bắc ireland": "GBP",
|
|
||||||
"kip": "LAK",
|
"kip": "LAK",
|
||||||
"kip laos": "LAK",
|
"kip laos": "LAK",
|
||||||
"kip laosiano": "LAK",
|
"kip laosiano": "LAK",
|
||||||
@@ -11192,7 +11141,6 @@
|
|||||||
"põhja korea won": "KPW",
|
"põhja korea won": "KPW",
|
||||||
"põhja makedoonia denaar": "MKD",
|
"põhja makedoonia denaar": "MKD",
|
||||||
"prata como investimento": "XAG",
|
"prata como investimento": "XAG",
|
||||||
"produits agricole de l'angleterre": "GBP",
|
|
||||||
"pula": "BWP",
|
"pula": "BWP",
|
||||||
"pula botswana": "BWP",
|
"pula botswana": "BWP",
|
||||||
"pula botswanais": "BWP",
|
"pula botswanais": "BWP",
|
||||||
@@ -11243,7 +11191,6 @@
|
|||||||
"qatarisk rial": "QAR",
|
"qatarisk rial": "QAR",
|
||||||
"qäpik": "AZN",
|
"qäpik": "AZN",
|
||||||
"qindarka": "ALL",
|
"qindarka": "ALL",
|
||||||
"quanza": "AOA",
|
|
||||||
"quetzal": "GTQ",
|
"quetzal": "GTQ",
|
||||||
"quetzal guatemala": "GTQ",
|
"quetzal guatemala": "GTQ",
|
||||||
"quetzal guatemalteco": "GTQ",
|
"quetzal guatemalteco": "GTQ",
|
||||||
@@ -11569,7 +11516,6 @@
|
|||||||
"rupia del pakistan": "PKR",
|
"rupia del pakistan": "PKR",
|
||||||
"rupia dell'india": "INR",
|
"rupia dell'india": "INR",
|
||||||
"rupia delle seychelles": "SCR",
|
"rupia delle seychelles": "SCR",
|
||||||
"rupia din seychelles": "SCR",
|
|
||||||
"rupia do nepal": "NPR",
|
"rupia do nepal": "NPR",
|
||||||
"rupia do paquistão": "PKR",
|
"rupia do paquistão": "PKR",
|
||||||
"rupia do seri lanca": "LKR",
|
"rupia do seri lanca": "LKR",
|
||||||
@@ -11625,7 +11571,6 @@
|
|||||||
],
|
],
|
||||||
"rupie indiană": "INR",
|
"rupie indiană": "INR",
|
||||||
"rupie indiane": "INR",
|
"rupie indiane": "INR",
|
||||||
"rupie seychelloză": "SCR",
|
|
||||||
"rupies índies": "INR",
|
"rupies índies": "INR",
|
||||||
"rupija": [
|
"rupija": [
|
||||||
"NPR",
|
"NPR",
|
||||||
@@ -12055,10 +12000,6 @@
|
|||||||
"sterliņu mārciņa": "GBP",
|
"sterliņu mārciņa": "GBP",
|
||||||
"stērliņu mārciņa": "GBP",
|
"stērliņu mārciņa": "GBP",
|
||||||
"stn": "STN",
|
"stn": "STN",
|
||||||
"storbritannien och irlands ekonomi": "GBP",
|
|
||||||
"storbritannien och nordirlands ekonomi": "GBP",
|
|
||||||
"storbritanniens ekonomi": "GBP",
|
|
||||||
"storbritanniens økonomi": "GBP",
|
|
||||||
"stredoafrický frank": "XAF",
|
"stredoafrický frank": "XAF",
|
||||||
"středoafrický frank": "XAF",
|
"středoafrický frank": "XAF",
|
||||||
"sucre": "XSU",
|
"sucre": "XSU",
|
||||||
@@ -12108,7 +12049,6 @@
|
|||||||
"suriye lirası": "SYP",
|
"suriye lirası": "SYP",
|
||||||
"suudi arabistan riyali": "SAR",
|
"suudi arabistan riyali": "SAR",
|
||||||
"suudi riyali": "SAR",
|
"suudi riyali": "SAR",
|
||||||
"suurbritannia majandus": "GBP",
|
|
||||||
"suurbritannia nael": "GBP",
|
"suurbritannia nael": "GBP",
|
||||||
"suurbritannia naelsterling": "GBP",
|
"suurbritannia naelsterling": "GBP",
|
||||||
"suvereni bolivar": "VES",
|
"suvereni bolivar": "VES",
|
||||||
@@ -12215,7 +12155,6 @@
|
|||||||
"švicarski frank": "CHF",
|
"švicarski frank": "CHF",
|
||||||
"švýcarský frank": "CHF",
|
"švýcarský frank": "CHF",
|
||||||
"șekel nou": "ILS",
|
"șekel nou": "ILS",
|
||||||
"șiling somalez": "SOS",
|
|
||||||
"şekel": "ILS",
|
"şekel": "ILS",
|
||||||
"şili pesosu": "CLP",
|
"şili pesosu": "CLP",
|
||||||
"s₣": "CHF",
|
"s₣": "CHF",
|
||||||
@@ -12558,8 +12497,6 @@
|
|||||||
"uguiya": "MRU",
|
"uguiya": "MRU",
|
||||||
"ugx": "UGX",
|
"ugx": "UGX",
|
||||||
"ui": "UYI",
|
"ui": "UYI",
|
||||||
"uk economy": "GBP",
|
|
||||||
"uk's economy": "GBP",
|
|
||||||
"ukl": "GBP",
|
"ukl": "GBP",
|
||||||
"ukraina grivna": "UAH",
|
"ukraina grivna": "UAH",
|
||||||
"ukraina hrivno": "UAH",
|
"ukraina hrivno": "UAH",
|
||||||
@@ -12600,8 +12537,6 @@
|
|||||||
"unidades de inversion": "MXV",
|
"unidades de inversion": "MXV",
|
||||||
"unidades de inversión": "MXV",
|
"unidades de inversión": "MXV",
|
||||||
"united arab emirates dirham": "AED",
|
"united arab emirates dirham": "AED",
|
||||||
"united kingdom economy": "GBP",
|
|
||||||
"united kingdom's economy": "GBP",
|
|
||||||
"united states dollar": [
|
"united states dollar": [
|
||||||
"USN",
|
"USN",
|
||||||
"USD"
|
"USD"
|
||||||
@@ -12703,7 +12638,6 @@
|
|||||||
"venemaa rubla": "RUB",
|
"venemaa rubla": "RUB",
|
||||||
"venezuelai bolívar": "VES",
|
"venezuelai bolívar": "VES",
|
||||||
"venezuelan digital bolívar": "VED",
|
"venezuelan digital bolívar": "VED",
|
||||||
"verenigd koninkrijk economie": "GBP",
|
|
||||||
"verenigde arabiese emirate dirham": "AED",
|
"verenigde arabiese emirate dirham": "AED",
|
||||||
"verenigde arabische emiraten dirham": "AED",
|
"verenigde arabische emiraten dirham": "AED",
|
||||||
"ves": "VES",
|
"ves": "VES",
|
||||||
@@ -12735,12 +12669,6 @@
|
|||||||
"wir euro": "CHE",
|
"wir euro": "CHE",
|
||||||
"wir franc": "CHW",
|
"wir franc": "CHW",
|
||||||
"wir franken": "CHW",
|
"wir franken": "CHW",
|
||||||
"wirtschaft": "GBP",
|
|
||||||
"wirtschaft des vereinigten königreichs": "GBP",
|
|
||||||
"wirtschaft im vereinigten königreich": "GBP",
|
|
||||||
"wirtschaft in dem vereinigten königreich": "GBP",
|
|
||||||
"wirtschaft vom vereinigten königreich": "GBP",
|
|
||||||
"wirtschaft von dem vereinigten königreich": "GBP",
|
|
||||||
"wit russische roebel": "BYN",
|
"wit russische roebel": "BYN",
|
||||||
"won": "KRW",
|
"won": "KRW",
|
||||||
"won bắc triều tiên": "KPW",
|
"won bắc triều tiên": "KPW",
|
||||||
@@ -12834,7 +12762,6 @@
|
|||||||
"yeşil burun adaları eskudosu": "CVE",
|
"yeşil burun adaları eskudosu": "CVE",
|
||||||
"yên nhật": "JPY",
|
"yên nhật": "JPY",
|
||||||
"yhdistyneen kuningaskunnan punta": "GBP",
|
"yhdistyneen kuningaskunnan punta": "GBP",
|
||||||
"yhdistyneen kuningaskunnan talous": "GBP",
|
|
||||||
"yhdistyneiden arabiemiraattien dirhami": "AED",
|
"yhdistyneiden arabiemiraattien dirhami": "AED",
|
||||||
"yhdysvaltain dollari": "USD",
|
"yhdysvaltain dollari": "USD",
|
||||||
"ytl": "TRY",
|
"ytl": "TRY",
|
||||||
@@ -13584,8 +13511,6 @@
|
|||||||
"египетский фунт": "EGP",
|
"египетский фунт": "EGP",
|
||||||
"единая система региональных взаиморасчётов": "XSU",
|
"единая система региональных взаиморасчётов": "XSU",
|
||||||
"единая система региональных взаиморасчетов": "XSU",
|
"единая система региональных взаиморасчетов": "XSU",
|
||||||
"економіка великобританії": "GBP",
|
|
||||||
"економіка великої британії": "GBP",
|
|
||||||
"енглеска фунта": "GBP",
|
"енглеска фунта": "GBP",
|
||||||
"еритрейська накфа": "ERN",
|
"еритрейська накфа": "ERN",
|
||||||
"еритрејска накфа": "ERN",
|
"еритрејска накфа": "ERN",
|
||||||
@@ -13642,8 +13567,6 @@
|
|||||||
"израелски шекел": "ILS",
|
"израелски шекел": "ILS",
|
||||||
"израильский новый шекель": "ILS",
|
"израильский новый шекель": "ILS",
|
||||||
"източнокарибски долар": "XCD",
|
"източнокарибски долар": "XCD",
|
||||||
"икономика на великобритания": "GBP",
|
|
||||||
"икономика на обединеното кралство": "GBP",
|
|
||||||
"индийска рупия": "INR",
|
"индийска рупия": "INR",
|
||||||
"индийская рупия": "INR",
|
"индийская рупия": "INR",
|
||||||
"индијска рупија": "INR",
|
"индијска рупија": "INR",
|
||||||
@@ -14057,7 +13980,6 @@
|
|||||||
"PLZ",
|
"PLZ",
|
||||||
"PLN"
|
"PLN"
|
||||||
],
|
],
|
||||||
"привреда уједињеног краљевства": "GBP",
|
|
||||||
"пула": "BWP",
|
"пула": "BWP",
|
||||||
"південно африканський ранд": "ZAR",
|
"південно африканський ранд": "ZAR",
|
||||||
"південнокорейська вона": "KRW",
|
"південнокорейська вона": "KRW",
|
||||||
@@ -14145,7 +14067,6 @@
|
|||||||
"севернокорејски вон": "KPW",
|
"севернокорејски вон": "KPW",
|
||||||
"северо корейская вона": "KPW",
|
"северо корейская вона": "KPW",
|
||||||
"северокорейская вона": "KPW",
|
"северокорейская вона": "KPW",
|
||||||
"седі": "GHS",
|
|
||||||
"сейшел рупиясе": "SCR",
|
"сейшел рупиясе": "SCR",
|
||||||
"сейшелска рупия": "SCR",
|
"сейшелска рупия": "SCR",
|
||||||
"сейшельская рупия": "SCR",
|
"сейшельская рупия": "SCR",
|
||||||
@@ -14198,8 +14119,6 @@
|
|||||||
"старый румынский лей": "RON",
|
"старый румынский лей": "RON",
|
||||||
"стерлинг фунты": "GBP",
|
"стерлинг фунты": "GBP",
|
||||||
"стерлиң фунты": "GBP",
|
"стерлиң фунты": "GBP",
|
||||||
"стопанство на великобритания": "GBP",
|
|
||||||
"стопанство на обединеното кралство": "GBP",
|
|
||||||
"суверен боливар": "VES",
|
"суверен боливар": "VES",
|
||||||
"суверенний болівар": "VES",
|
"суверенний болівар": "VES",
|
||||||
"суверенный боливар": "VES",
|
"суверенный боливар": "VES",
|
||||||
@@ -14450,7 +14369,6 @@
|
|||||||
"шриланкийска рупия": "LKR",
|
"шриланкийска рупия": "LKR",
|
||||||
"шриланчанска рупија": "LKR",
|
"шриланчанска рупија": "LKR",
|
||||||
"щатски долар": "USD",
|
"щатски долар": "USD",
|
||||||
"экономика великобритании": "GBP",
|
|
||||||
"эритрейская накфа": "ERN",
|
"эритрейская накфа": "ERN",
|
||||||
"эритрея накфасы": "ERN",
|
"эритрея накфасы": "ERN",
|
||||||
"эсватини лилангение": "SZL",
|
"эсватини лилангение": "SZL",
|
||||||
@@ -14600,8 +14518,6 @@
|
|||||||
"יואן סיני": "CNY",
|
"יואן סיני": "CNY",
|
||||||
"ין יפני": "JPY",
|
"ין יפני": "JPY",
|
||||||
"כארתולי לארי": "GEL",
|
"כארתולי לארי": "GEL",
|
||||||
"כלכלת בריטניה": "GBP",
|
|
||||||
"כלכלת הממלכה המאוחדת": "GBP",
|
|
||||||
"כתר דני": "DKK",
|
"כתר דני": "DKK",
|
||||||
"כתר נורבגי": "NOK",
|
"כתר נורבגי": "NOK",
|
||||||
"כתר נורווגי": "NOK",
|
"כתר נורווגי": "NOK",
|
||||||
@@ -14749,7 +14665,6 @@
|
|||||||
"استثمار البلاتين": "XPT",
|
"استثمار البلاتين": "XPT",
|
||||||
"استثمار الذهب": "XAU",
|
"استثمار الذهب": "XAU",
|
||||||
"استثمار الفضة": "XAG",
|
"استثمار الفضة": "XAG",
|
||||||
"اقتصاد المملكة المتحدة": "GBP",
|
|
||||||
"الاستثمار في الذهب": "XAU",
|
"الاستثمار في الذهب": "XAU",
|
||||||
"الأوقية الموريتانية": "MRU",
|
"الأوقية الموريتانية": "MRU",
|
||||||
"البات": "THB",
|
"البات": "THB",
|
||||||
@@ -14803,7 +14718,6 @@
|
|||||||
"أوقية": "MRU",
|
"أوقية": "MRU",
|
||||||
"أوقية موريتانية": "MRU",
|
"أوقية موريتانية": "MRU",
|
||||||
"أوقيه موريتانيه": "MRU",
|
"أوقيه موريتانيه": "MRU",
|
||||||
"إقتصاد بريطانى": "GBP",
|
|
||||||
"إيسكودو جزر الرأس الأخضر": "CVE",
|
"إيسكودو جزر الرأس الأخضر": "CVE",
|
||||||
"بات": "THB",
|
"بات": "THB",
|
||||||
"بات تايلاندي": "THB",
|
"بات تايلاندي": "THB",
|
||||||
@@ -15186,7 +15100,6 @@
|
|||||||
"মালদ্বীপীয় রুফিয়াহ": "MVR",
|
"মালদ্বীপীয় রুফিয়াহ": "MVR",
|
||||||
"মিয়ানমার ক্যত": "MMK",
|
"মিয়ানমার ক্যত": "MMK",
|
||||||
"মিশরীয় পাউন্ড": "EGP",
|
"মিশরীয় পাউন্ড": "EGP",
|
||||||
"যুক্তরাজ্যের অর্থনীতি": "GBP",
|
|
||||||
"রুশ রুবল": "RUB",
|
"রুশ রুবল": "RUB",
|
||||||
"রেনমিনবি": "CNY",
|
"রেনমিনবি": "CNY",
|
||||||
"রেন্মিন্বি": "CNY",
|
"রেন্মিন্বি": "CNY",
|
||||||
@@ -15818,7 +15731,6 @@
|
|||||||
"엔": "JPY",
|
"엔": "JPY",
|
||||||
"엔화": "JPY",
|
"엔화": "JPY",
|
||||||
"영국 파운드": "GBP",
|
"영국 파운드": "GBP",
|
||||||
"영국의 경제": "GBP",
|
|
||||||
"예멘 리알": "YER",
|
"예멘 리알": "YER",
|
||||||
"예멘 리얄": "YER",
|
"예멘 리얄": "YER",
|
||||||
"예멘리얄": "YER",
|
"예멘리얄": "YER",
|
||||||
@@ -16026,11 +15938,9 @@
|
|||||||
"イエメン・リアル": "YER",
|
"イエメン・リアル": "YER",
|
||||||
"イエメン・リヤル": "YER",
|
"イエメン・リヤル": "YER",
|
||||||
"イエメン・リヤール": "YER",
|
"イエメン・リヤール": "YER",
|
||||||
"イギリスの経済": "GBP",
|
|
||||||
"イギリスの通貨": "GBP",
|
"イギリスの通貨": "GBP",
|
||||||
"イギリスポンド": "GBP",
|
"イギリスポンド": "GBP",
|
||||||
"イギリス・ポンド": "GBP",
|
"イギリス・ポンド": "GBP",
|
||||||
"イギリス経済": "GBP",
|
|
||||||
"イラクの通貨": "IQD",
|
"イラクの通貨": "IQD",
|
||||||
"イラク・ディナール": "IQD",
|
"イラク・ディナール": "IQD",
|
||||||
"イランの通貨": "IRR",
|
"イランの通貨": "IRR",
|
||||||
@@ -16332,7 +16242,6 @@
|
|||||||
"英ポンド": "GBP",
|
"英ポンド": "GBP",
|
||||||
"西アフリカcfaフラン": "XOF",
|
"西アフリカcfaフラン": "XOF",
|
||||||
"豪ドル": "AUD",
|
"豪ドル": "AUD",
|
||||||
"財政・経済政策": "GBP",
|
|
||||||
"越南銅": "VND",
|
"越南銅": "VND",
|
||||||
"金投資": "XAU",
|
"金投資": "XAU",
|
||||||
"韓国ウォン": "KRW",
|
"韓国ウォン": "KRW",
|
||||||
|
|||||||
@@ -81,7 +81,6 @@
|
|||||||
"ca": "ca",
|
"ca": "ca",
|
||||||
"ckb": "ckb",
|
"ckb": "ckb",
|
||||||
"cs": "cs",
|
"cs": "cs",
|
||||||
"cv": "cv",
|
|
||||||
"cy": "cy",
|
"cy": "cy",
|
||||||
"da": "da",
|
"da": "da",
|
||||||
"de": "de",
|
"de": "de",
|
||||||
@@ -131,6 +130,7 @@
|
|||||||
"ne": "ne",
|
"ne": "ne",
|
||||||
"nl": "nl",
|
"nl": "nl",
|
||||||
"no": "no",
|
"no": "no",
|
||||||
|
"ny": "ny",
|
||||||
"om": "om",
|
"om": "om",
|
||||||
"pa": "pa",
|
"pa": "pa",
|
||||||
"pl": "pl",
|
"pl": "pl",
|
||||||
@@ -8446,7 +8446,6 @@
|
|||||||
"IR",
|
"IR",
|
||||||
"IS",
|
"IS",
|
||||||
"IT",
|
"IT",
|
||||||
"JE",
|
|
||||||
"JM",
|
"JM",
|
||||||
"JO",
|
"JO",
|
||||||
"JP",
|
"JP",
|
||||||
@@ -8476,7 +8475,6 @@
|
|||||||
"MC",
|
"MC",
|
||||||
"MD",
|
"MD",
|
||||||
"ME",
|
"ME",
|
||||||
"MF",
|
|
||||||
"MG",
|
"MG",
|
||||||
"MH",
|
"MH",
|
||||||
"MK",
|
"MK",
|
||||||
@@ -8543,7 +8541,6 @@
|
|||||||
"SS",
|
"SS",
|
||||||
"ST",
|
"ST",
|
||||||
"SV",
|
"SV",
|
||||||
"SX",
|
|
||||||
"SY",
|
"SY",
|
||||||
"SZ",
|
"SZ",
|
||||||
"TC",
|
"TC",
|
||||||
@@ -8691,7 +8688,6 @@
|
|||||||
"th": "thai",
|
"th": "thai",
|
||||||
"ti": "tigrinya",
|
"ti": "tigrinya",
|
||||||
"tk": "turkmen",
|
"tk": "turkmen",
|
||||||
"to": "tonga",
|
|
||||||
"tr": "turkish",
|
"tr": "turkish",
|
||||||
"tt": "tatar",
|
"tt": "tatar",
|
||||||
"uk": "ukrainian",
|
"uk": "ukrainian",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
],
|
],
|
||||||
"ua": "Mozilla/5.0 ({os}; rv:{version}) Gecko/20100101 Firefox/{version}",
|
"ua": "Mozilla/5.0 ({os}; rv:{version}) Gecko/20100101 Firefox/{version}",
|
||||||
"versions": [
|
"versions": [
|
||||||
"153.0",
|
"152.0",
|
||||||
"152.0"
|
"151.0"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -3521,7 +3521,7 @@
|
|||||||
},
|
},
|
||||||
"Q1377741": {
|
"Q1377741": {
|
||||||
"si_name": "Q25250",
|
"si_name": "Q25250",
|
||||||
"symbol": "V<sub>P</sub>",
|
"symbol": "V_P",
|
||||||
"to_si_factor": 1.0429e+27
|
"to_si_factor": 1.0429e+27
|
||||||
},
|
},
|
||||||
"Q1386162": {
|
"Q1386162": {
|
||||||
@@ -3694,11 +3694,6 @@
|
|||||||
"symbol": "apc",
|
"symbol": "apc",
|
||||||
"to_si_factor": 0.0308568
|
"to_si_factor": 0.0308568
|
||||||
},
|
},
|
||||||
"Q16068": {
|
|
||||||
"si_name": null,
|
|
||||||
"symbol": "DM",
|
|
||||||
"to_si_factor": null
|
|
||||||
},
|
|
||||||
"Q160857": {
|
"Q160857": {
|
||||||
"si_name": "Q25236",
|
"si_name": "Q25236",
|
||||||
"symbol": "hp",
|
"symbol": "hp",
|
||||||
@@ -3877,11 +3872,11 @@
|
|||||||
"Q180892": {
|
"Q180892": {
|
||||||
"si_name": "Q11570",
|
"si_name": "Q11570",
|
||||||
"symbol": "M☉",
|
"symbol": "M☉",
|
||||||
"to_si_factor": 1.988416e+30
|
"to_si_factor": 1.9884e+30
|
||||||
},
|
},
|
||||||
"Q1811": {
|
"Q1811": {
|
||||||
"si_name": "Q11573",
|
"si_name": "Q11573",
|
||||||
"symbol": "au",
|
"symbol": "AU",
|
||||||
"to_si_factor": 149597870700.0
|
"to_si_factor": 149597870700.0
|
||||||
},
|
},
|
||||||
"Q1815100": {
|
"Q1815100": {
|
||||||
@@ -4459,11 +4454,6 @@
|
|||||||
"symbol": "ng",
|
"symbol": "ng",
|
||||||
"to_si_factor": 1e-12
|
"to_si_factor": 1e-12
|
||||||
},
|
},
|
||||||
"Q2285395": {
|
|
||||||
"si_name": null,
|
|
||||||
"symbol": "dBW",
|
|
||||||
"to_si_factor": null
|
|
||||||
},
|
|
||||||
"Q22934083": {
|
"Q22934083": {
|
||||||
"si_name": "Q25406",
|
"si_name": "Q25406",
|
||||||
"symbol": "nC",
|
"symbol": "nC",
|
||||||
@@ -6326,7 +6316,7 @@
|
|||||||
},
|
},
|
||||||
"Q536785": {
|
"Q536785": {
|
||||||
"si_name": "Q844211",
|
"si_name": "Q844211",
|
||||||
"symbol": "ρ<sub>P</sub>",
|
"symbol": "ρ_P",
|
||||||
"to_si_factor": 5.155e+96
|
"to_si_factor": 5.155e+96
|
||||||
},
|
},
|
||||||
"Q53679433": {
|
"Q53679433": {
|
||||||
@@ -6981,7 +6971,7 @@
|
|||||||
},
|
},
|
||||||
"Q685662": {
|
"Q685662": {
|
||||||
"si_name": "Q44395",
|
"si_name": "Q44395",
|
||||||
"symbol": "p<sub>P</sub>",
|
"symbol": "p_P",
|
||||||
"to_si_factor": 4.633e+113
|
"to_si_factor": 4.633e+113
|
||||||
},
|
},
|
||||||
"Q686163": {
|
"Q686163": {
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ fragment SXNG_query on Query {
|
|||||||
|
|
||||||
def setup(_) -> bool:
|
def setup(_) -> bool:
|
||||||
global SXNG_query # pylint: disable=global-statement
|
global SXNG_query # pylint: disable=global-statement
|
||||||
rand_str: str = "".join(random.choices(string.ascii_letters, k=5))
|
rand_str: str = "".join(random.choice(string.ascii_letters) for _ in range(5))
|
||||||
SXNG_query = SXNG_query.replace("SXNG_query", "PhotoSearchPaginationContainer_query_1" + rand_str)
|
SXNG_query = SXNG_query.replace("SXNG_query", "PhotoSearchPaginationContainer_query_1" + rand_str)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ base_url = "https://api.bilibili.com/x/web-interface/search/type"
|
|||||||
|
|
||||||
cookie = {
|
cookie = {
|
||||||
"innersign": "0",
|
"innersign": "0",
|
||||||
"buvid3": "".join(random.choices(string.hexdigits, k=16)) + "infoc",
|
"buvid3": "".join(random.choice(string.hexdigits) for _ in range(16)) + "infoc",
|
||||||
"i-wanna-go-back": "-1",
|
"i-wanna-go-back": "-1",
|
||||||
"b_ut": "7",
|
"b_ut": "7",
|
||||||
"FEED_LIVE_VERSION": "V8",
|
"FEED_LIVE_VERSION": "V8",
|
||||||
|
|||||||
@@ -43,10 +43,9 @@ def init(_):
|
|||||||
|
|
||||||
def request(query: str, params: "OnlineParams"):
|
def request(query: str, params: "OnlineParams"):
|
||||||
params["url"] = f"{base_url}/api/{dogpile_categ}"
|
params["url"] = f"{base_url}/api/{dogpile_categ}"
|
||||||
params["headers"]["Origin"] = base_url
|
|
||||||
|
|
||||||
params["method"] = "POST"
|
params["method"] = "POST"
|
||||||
params["json"] = {"q": query, "qadf": safe_search_map[params["safesearch"]], "page": params["pageno"]}
|
params["json"] = {"q": query, "qadf": safe_search_map[params["safesearch"]], "page": params["pageno"]}
|
||||||
|
return params
|
||||||
|
|
||||||
|
|
||||||
def response(resp: "SXNG_Response"):
|
def response(resp: "SXNG_Response"):
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
"""Engine to search using the official `Exa Search API`_. Exa is a search engine for AI agents.
|
|
||||||
|
|
||||||
.. _Exa Search API: https://exa.ai/docs/reference/search
|
|
||||||
|
|
||||||
Configuration
|
|
||||||
=============
|
|
||||||
|
|
||||||
The engine has the following mandatory setting:
|
|
||||||
|
|
||||||
- :py:obj:`api_key`
|
|
||||||
|
|
||||||
You can obtain an API key from the `API Key section <https://dashboard.exa.ai/api-keys>`_ in the Exa dashboard.
|
|
||||||
|
|
||||||
Optional settings are:
|
|
||||||
|
|
||||||
- :py:obj:`results_per_page`
|
|
||||||
- :py:obj:`search_type`
|
|
||||||
- :py:obj:`content_mode`
|
|
||||||
- :py:obj:`content_max_characters`
|
|
||||||
|
|
||||||
.. code:: yaml
|
|
||||||
|
|
||||||
- name: exaapi
|
|
||||||
engine: exaapi
|
|
||||||
shortcut: exa
|
|
||||||
api_key: "..."
|
|
||||||
results_per_page: 10
|
|
||||||
search_type: auto
|
|
||||||
content_mode: highlights
|
|
||||||
inactive: false
|
|
||||||
|
|
||||||
The API supports SafeSearch and region-aware results.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import typing as t
|
|
||||||
|
|
||||||
from dateutil import parser
|
|
||||||
|
|
||||||
from searx.exceptions import SearxEngineAPIException
|
|
||||||
from searx.result_types import EngineResults
|
|
||||||
from searx.utils import html_to_text
|
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
|
||||||
from searx.extended_types import SXNG_Response
|
|
||||||
from searx.search.processors import OnlineParams
|
|
||||||
|
|
||||||
|
|
||||||
SearchType = t.Literal["fast", "auto", "instant", "deep", "deep-lite", "deep-reasoning"]
|
|
||||||
ContentMode = t.Literal["highlights", "text"]
|
|
||||||
|
|
||||||
about = {
|
|
||||||
"website": "https://exa.ai",
|
|
||||||
"wikidata_id": None,
|
|
||||||
"official_api_documentation": "https://exa.ai/docs/reference/search",
|
|
||||||
"use_official_api": True,
|
|
||||||
"require_api_key": True,
|
|
||||||
"results": "JSON",
|
|
||||||
}
|
|
||||||
|
|
||||||
api_key: str = ""
|
|
||||||
"""API key for Exa Search API (required)."""
|
|
||||||
|
|
||||||
categories = ["general", "web"]
|
|
||||||
safesearch = True
|
|
||||||
|
|
||||||
base_url = "https://api.exa.ai/search"
|
|
||||||
results_per_page: int = 10
|
|
||||||
"""Maximum number of results per request. Value must be between 1 and 100, default is 10."""
|
|
||||||
|
|
||||||
search_type: SearchType = "auto"
|
|
||||||
"""Search type. Default is auto, see documentation for more information."""
|
|
||||||
|
|
||||||
content_mode: ContentMode = "highlights"
|
|
||||||
"""Content to request from the API: ``highlights`` (excerpts) or ``text`` (page text)."""
|
|
||||||
|
|
||||||
content_max_characters: int = 500
|
|
||||||
"""Maximum characters for the requested content."""
|
|
||||||
|
|
||||||
|
|
||||||
def init(_):
|
|
||||||
if not api_key:
|
|
||||||
raise SearxEngineAPIException("No API key provided")
|
|
||||||
if not 1 <= results_per_page <= 100:
|
|
||||||
raise ValueError("results_per_page must be between 1 and 100")
|
|
||||||
if search_type not in t.get_args(SearchType):
|
|
||||||
raise ValueError(f"Unsupported search type: {search_type}")
|
|
||||||
if content_mode not in t.get_args(ContentMode):
|
|
||||||
raise ValueError(f"Unsupported content mode: {content_mode}")
|
|
||||||
if content_max_characters < 1:
|
|
||||||
raise ValueError("content_max_characters must be at least 1")
|
|
||||||
|
|
||||||
|
|
||||||
def _contents_payload() -> dict[str, t.Any]:
|
|
||||||
if content_mode == "text":
|
|
||||||
return {"text": {"maxCharacters": content_max_characters, "stripLinks": True}}
|
|
||||||
return {"highlights": {"maxCharacters": content_max_characters}}
|
|
||||||
|
|
||||||
|
|
||||||
def _extract_content(result: dict[str, t.Any]) -> str:
|
|
||||||
if content_mode == "text":
|
|
||||||
return html_to_text(result.get("text") or "")
|
|
||||||
return html_to_text(" ".join(result.get("highlights") or []))
|
|
||||||
|
|
||||||
|
|
||||||
def request(query: str, params: "OnlineParams") -> None:
|
|
||||||
"""Create the API request."""
|
|
||||||
body: dict[str, t.Any] = {
|
|
||||||
"query": query,
|
|
||||||
"type": search_type,
|
|
||||||
"numResults": results_per_page,
|
|
||||||
"contents": _contents_payload(),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Apply SafeSearch if enabled
|
|
||||||
if params["safesearch"]:
|
|
||||||
body["moderation"] = True
|
|
||||||
|
|
||||||
# Apply region-aware results if specified
|
|
||||||
locale_parts = params["searxng_locale"].split("-")
|
|
||||||
region = locale_parts[-1]
|
|
||||||
if len(locale_parts) > 1:
|
|
||||||
body["userLocation"] = region.upper()
|
|
||||||
|
|
||||||
params["url"] = base_url
|
|
||||||
params["method"] = "POST"
|
|
||||||
params["headers"]["x-api-key"] = api_key
|
|
||||||
params["json"] = body
|
|
||||||
|
|
||||||
|
|
||||||
def _extract_published_date(value: str | None):
|
|
||||||
"""Extract and parse the published date from the API response.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
value: Raw date string from the API
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Parsed datetime object or None if parsing fails
|
|
||||||
"""
|
|
||||||
if not value:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
return parser.parse(value)
|
|
||||||
except (parser.ParserError, TypeError, OverflowError):
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def response(resp: "SXNG_Response") -> EngineResults:
|
|
||||||
"""Process the API response and return results."""
|
|
||||||
res = EngineResults()
|
|
||||||
|
|
||||||
for result in resp.json().get("results", []):
|
|
||||||
url = result.get("url")
|
|
||||||
if not url:
|
|
||||||
continue
|
|
||||||
|
|
||||||
res.add(
|
|
||||||
res.types.MainResult(
|
|
||||||
url=url,
|
|
||||||
title=html_to_text(result.get("title") or url),
|
|
||||||
content=_extract_content(result),
|
|
||||||
thumbnail=result.get("image") or "",
|
|
||||||
publishedDate=_extract_published_date(result.get("publishedDate")),
|
|
||||||
author=result.get("author") or "",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return res
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
"""Jina is a search AI and part of Elastic, the company behind ElasticSearch.
|
|
||||||
|
|
||||||
The engine requires an API key, you can get one from the
|
|
||||||
`API dashboard <https://jina.ai/api-dashboard/>`_ without signup.
|
|
||||||
|
|
||||||
.. code:: yaml
|
|
||||||
|
|
||||||
- name: jina
|
|
||||||
engine: jina
|
|
||||||
shortcut: ji
|
|
||||||
api_key: "jina_..."
|
|
||||||
jina_engine: reader
|
|
||||||
inactive: false
|
|
||||||
|
|
||||||
By default, Jina's own index is used. You can change that by setting a different :py:obj:`jina_engine`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import typing as t
|
|
||||||
from urllib.parse import urlencode
|
|
||||||
|
|
||||||
from dateutil import parser
|
|
||||||
from searx.result_types import EngineResults
|
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
|
||||||
from searx.extended_types import SXNG_Response
|
|
||||||
from searx.search.processors import OnlineParams
|
|
||||||
|
|
||||||
|
|
||||||
about = {
|
|
||||||
"website": "https://jina.ai",
|
|
||||||
"wikidata_id": None,
|
|
||||||
"official_api_documentation": "https://s.jina.ai/docs",
|
|
||||||
"use_official_api": True,
|
|
||||||
"require_api_key": True,
|
|
||||||
"results": "JSON",
|
|
||||||
}
|
|
||||||
|
|
||||||
categories = ["general"]
|
|
||||||
paging = True
|
|
||||||
|
|
||||||
jina_engine = "reader"
|
|
||||||
"""Search mode. Currently supported values are 'reader', 'google' and 'bing'."""
|
|
||||||
|
|
||||||
base_url = "https://s.jina.ai"
|
|
||||||
api_key: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
def setup(_):
|
|
||||||
if not api_key:
|
|
||||||
raise ValueError("missing api key")
|
|
||||||
|
|
||||||
|
|
||||||
def request(query: str, params: "OnlineParams"):
|
|
||||||
# setting 'no-content' pushes the response time down to a third
|
|
||||||
args = {"q": query, "page": params["pageno"], "engine": jina_engine, "respondWith": "no-content"}
|
|
||||||
params["url"] = f"{base_url}/?{urlencode(args)}"
|
|
||||||
params["headers"].update(
|
|
||||||
{
|
|
||||||
"Accept": "application/json",
|
|
||||||
"Authorization": f"Bearer {api_key}",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def response(resp: "SXNG_Response"):
|
|
||||||
res = EngineResults()
|
|
||||||
|
|
||||||
json_resp: dict[str, t.Any] = resp.json()
|
|
||||||
|
|
||||||
result: dict[str, str]
|
|
||||||
for result in json_resp["data"]:
|
|
||||||
published_date = None
|
|
||||||
if result.get("date"):
|
|
||||||
try:
|
|
||||||
published_date = parser.parse(result["date"])
|
|
||||||
except parser.ParserError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
res.add(
|
|
||||||
res.types.MainResult(
|
|
||||||
url=result["url"],
|
|
||||||
title=result["title"],
|
|
||||||
content=result["description"],
|
|
||||||
publishedDate=published_date,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return res
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
"""Keenable is a fast web search with keyless mode support"""
|
|
||||||
|
|
||||||
import typing as t
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from searx.extended_types import SXNG_Response
|
|
||||||
from searx.result_types import EngineResults
|
|
||||||
from searx.utils import searxng_useragent
|
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
|
||||||
from searx.search.processors import OnlineParams
|
|
||||||
|
|
||||||
about = {
|
|
||||||
"website": "https://keenable.ai",
|
|
||||||
"official_api_documentation": "https://docs.keenable.ai",
|
|
||||||
"use_official_api": True,
|
|
||||||
"require_api_key": False,
|
|
||||||
"results": "JSON",
|
|
||||||
}
|
|
||||||
api_key = ""
|
|
||||||
""" Optional API Key. You can create a key at `the official website
|
|
||||||
<https://keenable.ai/signup>'_ if you need higher rate limits."""
|
|
||||||
|
|
||||||
categories = ["general"]
|
|
||||||
|
|
||||||
base_url = "https://api.keenable.ai"
|
|
||||||
keenable_mode = "pro"
|
|
||||||
|
|
||||||
|
|
||||||
def request(query: str, params: "OnlineParams"):
|
|
||||||
if api_key:
|
|
||||||
params["url"] = f"{base_url}/v1/search"
|
|
||||||
params["headers"]["X-API-KEY"] = api_key
|
|
||||||
else:
|
|
||||||
params["url"] = f"{base_url}/v1/search/public"
|
|
||||||
|
|
||||||
params["method"] = "POST"
|
|
||||||
params["headers"]["X-Keenable-Title"] = searxng_useragent()
|
|
||||||
params["json"] = {"query": query, "mode": keenable_mode}
|
|
||||||
|
|
||||||
|
|
||||||
def response(resp: "SXNG_Response") -> EngineResults:
|
|
||||||
res = EngineResults()
|
|
||||||
|
|
||||||
results: list[dict[str, str]] = resp.json()["results"] # type: ignore[reportAny]
|
|
||||||
|
|
||||||
for result in results:
|
|
||||||
published = None
|
|
||||||
pub = result.get("published_at")
|
|
||||||
if pub:
|
|
||||||
try:
|
|
||||||
published = datetime.fromisoformat(pub.rstrip("Z"))
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
res.add(
|
|
||||||
res.types.MainResult(
|
|
||||||
url=result["url"],
|
|
||||||
title=result["title"],
|
|
||||||
content=result["description"] or result["snippet"],
|
|
||||||
publishedDate=published,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return res
|
|
||||||
@@ -0,0 +1,305 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
"""Presearch supports the search types listed in :py:obj:`search_type` (general,
|
||||||
|
images, videos, news).
|
||||||
|
|
||||||
|
Configured ``presarch`` engines:
|
||||||
|
|
||||||
|
.. code:: yaml
|
||||||
|
|
||||||
|
- name: presearch
|
||||||
|
engine: presearch
|
||||||
|
search_type: search
|
||||||
|
categories: [general, web]
|
||||||
|
|
||||||
|
- name: presearch images
|
||||||
|
...
|
||||||
|
search_type: images
|
||||||
|
categories: [images, web]
|
||||||
|
|
||||||
|
- name: presearch videos
|
||||||
|
...
|
||||||
|
search_type: videos
|
||||||
|
categories: [general, web]
|
||||||
|
|
||||||
|
- name: presearch news
|
||||||
|
...
|
||||||
|
search_type: news
|
||||||
|
categories: [news, web]
|
||||||
|
|
||||||
|
.. hint::
|
||||||
|
|
||||||
|
By default Presearch's video category is intentionally placed into::
|
||||||
|
|
||||||
|
categories: [general, web]
|
||||||
|
|
||||||
|
|
||||||
|
Search type ``video``
|
||||||
|
=====================
|
||||||
|
|
||||||
|
The results in the video category are most often links to pages that contain a
|
||||||
|
video, for instance many links from Preasearch's video category link content
|
||||||
|
from facebook (aka Meta) or Twitter (aka X). Since these are not real links to
|
||||||
|
video streams SearXNG can't use the video template for this and if SearXNG can't
|
||||||
|
use this template, then the user doesn't want to see these hits in the videos
|
||||||
|
category.
|
||||||
|
|
||||||
|
|
||||||
|
Languages & Regions
|
||||||
|
===================
|
||||||
|
|
||||||
|
In Presearch there are languages for the UI and regions for narrowing down the
|
||||||
|
search. If we set "auto" for the region in the WEB-UI of Presearch and cookie
|
||||||
|
``use_local_search_results=false``, then the defaults are set for both (the
|
||||||
|
language and the region) from the ``Accept-Language`` header.
|
||||||
|
|
||||||
|
Since the region is already "auto" by default, we only need to set the
|
||||||
|
``use_local_search_results`` cookie and send the ``Accept-Language`` header. We
|
||||||
|
have to set these values in both requests we send to Presearch; in the first
|
||||||
|
request to get the request-ID from Presearch and in the final request to get the
|
||||||
|
result list.
|
||||||
|
|
||||||
|
The time format returned by Presearch varies depending on the language set.
|
||||||
|
Multiple different formats can be supported by using ``dateutil`` parser, but
|
||||||
|
it doesn't support formats such as "N time ago", "vor N time" (German),
|
||||||
|
"Hace N time" (Spanish). Because of this, the dates are simply joined together
|
||||||
|
with the rest of other metadata.
|
||||||
|
|
||||||
|
|
||||||
|
Implementations
|
||||||
|
===============
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from urllib.parse import urlencode, urlparse
|
||||||
|
from searx import locales
|
||||||
|
from searx.network import get
|
||||||
|
from searx.utils import gen_useragent, html_to_text, parse_duration_string
|
||||||
|
|
||||||
|
about = {
|
||||||
|
"website": "https://presearch.io",
|
||||||
|
"wikidata_id": "Q7240905",
|
||||||
|
"official_api_documentation": "https://docs.presearch.io/nodes/api",
|
||||||
|
"use_official_api": False,
|
||||||
|
"require_api_key": False,
|
||||||
|
"results": "JSON",
|
||||||
|
}
|
||||||
|
paging = True
|
||||||
|
safesearch = True
|
||||||
|
time_range_support = True
|
||||||
|
categories = ["general", "web"] # general, images, videos, news
|
||||||
|
|
||||||
|
# HTTP2 requests immediately get blocked by a CAPTCHA
|
||||||
|
enable_http2 = False
|
||||||
|
|
||||||
|
search_type = "search"
|
||||||
|
"""must be any of ``search``, ``images``, ``videos``, ``news``"""
|
||||||
|
|
||||||
|
base_url = "https://presearch.com"
|
||||||
|
safesearch_map = {0: 'false', 1: 'true', 2: 'true'}
|
||||||
|
|
||||||
|
|
||||||
|
def init(_):
|
||||||
|
if search_type not in ['search', 'images', 'videos', 'news']:
|
||||||
|
raise ValueError(f'presearch search_type: {search_type}')
|
||||||
|
|
||||||
|
|
||||||
|
def _get_request_id(query, params):
|
||||||
|
|
||||||
|
args = {
|
||||||
|
"q": query,
|
||||||
|
"page": params["pageno"],
|
||||||
|
}
|
||||||
|
|
||||||
|
if params["time_range"]:
|
||||||
|
args["time"] = params["time_range"]
|
||||||
|
|
||||||
|
url = f"{base_url}/{search_type}?{urlencode(args)}"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'User-Agent': gen_useragent(),
|
||||||
|
'Cookie': (
|
||||||
|
f"b=1;"
|
||||||
|
f" presearch_session=;"
|
||||||
|
f" use_local_search_results=false;"
|
||||||
|
f" use_safe_search={safesearch_map[params['safesearch']]}"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
if params['searxng_locale'] != 'all':
|
||||||
|
l = locales.get_locale(params['searxng_locale'])
|
||||||
|
|
||||||
|
# Presearch narrows down the search by region. In SearXNG when the user
|
||||||
|
# does not set a region (e.g. 'en-CA' / canada) we cannot hand over a region.
|
||||||
|
|
||||||
|
# We could possibly use searx.locales.get_official_locales to determine
|
||||||
|
# in which regions this language is an official one, but then we still
|
||||||
|
# wouldn't know which region should be given more weight / Presearch
|
||||||
|
# performs an IP-based geolocation of the user, we don't want that in
|
||||||
|
# SearXNG ;-)
|
||||||
|
|
||||||
|
if l and l.territory:
|
||||||
|
headers['Accept-Language'] = f"{l.language}-{l.territory},{l.language};" "q=0.9,*;" "q=0.5"
|
||||||
|
|
||||||
|
resp = get(url, headers=headers, timeout=5)
|
||||||
|
|
||||||
|
for line in resp.text.split("\n"):
|
||||||
|
if "window.searchId = " in line:
|
||||||
|
return line.split("= ")[1][:-1].replace('"', ""), resp.cookies
|
||||||
|
|
||||||
|
raise RuntimeError("Couldn't find any request id for presearch")
|
||||||
|
|
||||||
|
|
||||||
|
def request(query, params):
|
||||||
|
request_id, cookies = _get_request_id(query, params)
|
||||||
|
params["headers"]["Accept"] = "application/json"
|
||||||
|
params["url"] = f"{base_url}/results?id={request_id}"
|
||||||
|
params["cookies"] = cookies
|
||||||
|
|
||||||
|
return params
|
||||||
|
|
||||||
|
|
||||||
|
def _strip_leading_strings(text):
|
||||||
|
for x in ['wikipedia', 'google']:
|
||||||
|
if text.lower().endswith(x):
|
||||||
|
text = text[: -len(x)]
|
||||||
|
return text.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _fix_title(title, url):
|
||||||
|
"""
|
||||||
|
Titles from Presearch shows domain + title without spacing, and HTML
|
||||||
|
This function removes these 2 issues.
|
||||||
|
Transforming "translate.google.co.in<em>Google</em> Translate" into "Google Translate"
|
||||||
|
"""
|
||||||
|
parsed_url = urlparse(url)
|
||||||
|
domain = parsed_url.netloc
|
||||||
|
title = html_to_text(title)
|
||||||
|
# Fixes issue where domain would show up in the title
|
||||||
|
# translate.google.co.inGoogle Translate -> Google Translate
|
||||||
|
if (
|
||||||
|
title.startswith(domain)
|
||||||
|
and len(title) > len(domain)
|
||||||
|
and not title.startswith(domain + "/")
|
||||||
|
and not title.startswith(domain + " ")
|
||||||
|
):
|
||||||
|
title = title.removeprefix(domain)
|
||||||
|
return title
|
||||||
|
|
||||||
|
|
||||||
|
def parse_search_query(json_results):
|
||||||
|
results = []
|
||||||
|
if not json_results:
|
||||||
|
return results
|
||||||
|
|
||||||
|
for item in json_results.get('specialSections', {}).get('topStoriesCompact', {}).get('data', []):
|
||||||
|
result = {
|
||||||
|
'url': item['link'],
|
||||||
|
'title': _fix_title(item['title'], item['link']),
|
||||||
|
'thumbnail': item['image'],
|
||||||
|
'content': '',
|
||||||
|
'metadata': item.get('source'),
|
||||||
|
}
|
||||||
|
results.append(result)
|
||||||
|
|
||||||
|
for item in json_results.get('standardResults', []):
|
||||||
|
result = {
|
||||||
|
'url': item['link'],
|
||||||
|
'title': _fix_title(item['title'], item['link']),
|
||||||
|
'content': html_to_text(item['description']),
|
||||||
|
}
|
||||||
|
results.append(result)
|
||||||
|
|
||||||
|
info = json_results.get('infoSection', {}).get('data')
|
||||||
|
if info:
|
||||||
|
attributes = []
|
||||||
|
for item in info.get('about', []):
|
||||||
|
|
||||||
|
text = html_to_text(item)
|
||||||
|
if ':' in text:
|
||||||
|
# split text into key / value
|
||||||
|
label, value = text.split(':', 1)
|
||||||
|
else:
|
||||||
|
# In other languages (tested with zh-TW) a colon is represented
|
||||||
|
# by a different symbol --> then we split at the first space.
|
||||||
|
label, value = text.split(' ', 1)
|
||||||
|
label = label[:-1]
|
||||||
|
|
||||||
|
value = _strip_leading_strings(value)
|
||||||
|
attributes.append({'label': label, 'value': value})
|
||||||
|
content = []
|
||||||
|
for item in [info.get('subtitle'), info.get('description')]:
|
||||||
|
if not item:
|
||||||
|
continue
|
||||||
|
item = _strip_leading_strings(html_to_text(item))
|
||||||
|
if item:
|
||||||
|
content.append(item)
|
||||||
|
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
'infobox': info['title'],
|
||||||
|
'id': info['title'],
|
||||||
|
'img_src': info.get('image'),
|
||||||
|
'content': ' | '.join(content),
|
||||||
|
'attributes': attributes,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def response(resp):
|
||||||
|
results = []
|
||||||
|
json_resp = resp.json()
|
||||||
|
|
||||||
|
if search_type == 'search':
|
||||||
|
results = parse_search_query(json_resp.get('results', {}))
|
||||||
|
|
||||||
|
elif search_type == 'images':
|
||||||
|
for item in json_resp.get('images', []):
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
'template': 'images.html',
|
||||||
|
'title': html_to_text(item['title']),
|
||||||
|
'url': item.get('link'),
|
||||||
|
'img_src': item.get('image'),
|
||||||
|
'thumbnail_src': item.get('thumbnail'),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
elif search_type == 'videos':
|
||||||
|
# The results in the video category are most often links to pages that contain
|
||||||
|
# a video and not to a video stream --> SearXNG can't use the video template.
|
||||||
|
|
||||||
|
for item in json_resp.get('videos', []):
|
||||||
|
duration = item.get('duration')
|
||||||
|
if duration:
|
||||||
|
duration = parse_duration_string(duration)
|
||||||
|
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
'title': html_to_text(item['title']),
|
||||||
|
'url': item.get('link'),
|
||||||
|
'content': item.get('description', ''),
|
||||||
|
'thumbnail': item.get('image'),
|
||||||
|
'length': duration,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
elif search_type == 'news':
|
||||||
|
for item in json_resp.get('news', []):
|
||||||
|
source = item.get('source')
|
||||||
|
# Bug on their end, time sometimes returns "</a>"
|
||||||
|
time = html_to_text(item.get('time')).strip()
|
||||||
|
metadata = [source]
|
||||||
|
if time != "":
|
||||||
|
metadata.append(time)
|
||||||
|
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
'title': html_to_text(item['title']),
|
||||||
|
'url': item.get('link'),
|
||||||
|
'content': html_to_text(item.get('description', '')),
|
||||||
|
'metadata': ' / '.join(metadata),
|
||||||
|
'thumbnail': item.get('image'),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return results
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
"""Reddit"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
from urllib.parse import urlencode, urljoin, urlparse
|
||||||
|
|
||||||
|
# about
|
||||||
|
about = {
|
||||||
|
"website": 'https://www.reddit.com/',
|
||||||
|
"wikidata_id": 'Q1136',
|
||||||
|
"official_api_documentation": 'https://www.reddit.com/dev/api',
|
||||||
|
"use_official_api": True,
|
||||||
|
"require_api_key": False,
|
||||||
|
"results": 'JSON',
|
||||||
|
}
|
||||||
|
|
||||||
|
# engine dependent config
|
||||||
|
categories = ['social media']
|
||||||
|
page_size = 25
|
||||||
|
|
||||||
|
# search-url
|
||||||
|
base_url = 'https://www.reddit.com/'
|
||||||
|
search_url = base_url + 'search.json?{query}'
|
||||||
|
|
||||||
|
|
||||||
|
def request(query, params):
|
||||||
|
|
||||||
|
query = urlencode({'q': query, 'limit': page_size})
|
||||||
|
params['url'] = search_url.format(query=query)
|
||||||
|
|
||||||
|
return params
|
||||||
|
|
||||||
|
|
||||||
|
def response(resp):
|
||||||
|
|
||||||
|
img_results = []
|
||||||
|
text_results = []
|
||||||
|
|
||||||
|
search_results = json.loads(resp.text)
|
||||||
|
|
||||||
|
# return empty array if there are no results
|
||||||
|
if 'data' not in search_results:
|
||||||
|
return []
|
||||||
|
|
||||||
|
posts = search_results.get('data', {}).get('children', [])
|
||||||
|
|
||||||
|
# process results
|
||||||
|
for post in posts:
|
||||||
|
data = post['data']
|
||||||
|
|
||||||
|
# extract post information
|
||||||
|
params = {'url': urljoin(base_url, data['permalink']), 'title': data['title']}
|
||||||
|
|
||||||
|
# if thumbnail field contains a valid URL, we need to change template
|
||||||
|
thumbnail = data['thumbnail']
|
||||||
|
url_info = urlparse(thumbnail)
|
||||||
|
# netloc & path
|
||||||
|
if url_info[1] != '' and url_info[2] != '':
|
||||||
|
params['img_src'] = data['url']
|
||||||
|
params['thumbnail_src'] = thumbnail
|
||||||
|
params['template'] = 'images.html'
|
||||||
|
img_results.append(params)
|
||||||
|
else:
|
||||||
|
created = datetime.fromtimestamp(data['created_utc'])
|
||||||
|
content = data['selftext']
|
||||||
|
if len(content) > 500:
|
||||||
|
content = content[:500] + '...'
|
||||||
|
params['content'] = content
|
||||||
|
params['publishedDate'] = created
|
||||||
|
text_results.append(params)
|
||||||
|
|
||||||
|
# show images first and text results second
|
||||||
|
return img_results + text_results
|
||||||
@@ -9,7 +9,6 @@ import codecs
|
|||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
import string
|
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
@@ -44,8 +43,8 @@ paging = True
|
|||||||
|
|
||||||
base_url = "https://api.swisscows.com"
|
base_url = "https://api.swisscows.com"
|
||||||
|
|
||||||
CAESAR_ALPHABET = string.ascii_uppercase
|
CAESAR_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
NONCE_ALPHABET = string.ascii_letters + string.digits + "-._~"
|
NONCE_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
|
||||||
|
|
||||||
time_range_map = {"day": "Day", "week": "Week", "month": "Month", "year": "Year"}
|
time_range_map = {"day": "Day", "week": "Week", "month": "Month", "year": "Year"}
|
||||||
|
|
||||||
@@ -93,7 +92,7 @@ def generate_nonce(length: int = 32) -> str:
|
|||||||
"""
|
"""
|
||||||
Generate a random char sequence with the given length.
|
Generate a random char sequence with the given length.
|
||||||
"""
|
"""
|
||||||
return "".join(random.choices(NONCE_ALPHABET, k=length))
|
return "".join([random.choice(NONCE_ALPHABET) for _ in range(length)])
|
||||||
|
|
||||||
|
|
||||||
def caesar_shift_with_switch_case(s: str, offset: int = 13) -> str:
|
def caesar_shift_with_switch_case(s: str, offset: int = 13) -> str:
|
||||||
|
|||||||
@@ -1,217 +0,0 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
"""Yandex Search API (the official **paid** `Yandex Search API v2`_).
|
|
||||||
|
|
||||||
Unlike the :origin:`yandex <searx/engines/yandex.py>` engine (which scrapes the
|
|
||||||
public HTML interface and is prone to CAPTCHA blocking), this engine talks to
|
|
||||||
the official, paid Yandex Cloud Search API. It requires a Yandex Cloud account,
|
|
||||||
a *folder id* and an *API key*.
|
|
||||||
|
|
||||||
The API answers with a Base64-encoded XML document, which is decoded and
|
|
||||||
parsed here.
|
|
||||||
|
|
||||||
Configuration
|
|
||||||
=============
|
|
||||||
|
|
||||||
The engine is inactive by default because it needs credentials. To enable it,
|
|
||||||
set ``inactive: false`` and add your ``api_key`` and ``yandex_folder_id`` to
|
|
||||||
:origin:`searx/settings.yml`:
|
|
||||||
|
|
||||||
.. code:: yaml
|
|
||||||
|
|
||||||
- name: yandex api
|
|
||||||
engine: yandex_api
|
|
||||||
shortcut: yda
|
|
||||||
categories: [general, web]
|
|
||||||
inactive: false
|
|
||||||
api_key: "" # Yandex Cloud API key (``Api-Key``)
|
|
||||||
yandex_folder_id: "" # Yandex Cloud folder id
|
|
||||||
# optional, see below:
|
|
||||||
yandex_default_language: en
|
|
||||||
|
|
||||||
.. _Yandex Search API v2:
|
|
||||||
https://aistudio.yandex.ru/docs/en/search-api/api-ref/WebSearch/search.html
|
|
||||||
"""
|
|
||||||
|
|
||||||
import math
|
|
||||||
import typing as t
|
|
||||||
from base64 import b64decode
|
|
||||||
|
|
||||||
from lxml import etree
|
|
||||||
|
|
||||||
from searx.exceptions import SearxEngineAPIException
|
|
||||||
from searx.result_types import EngineResults
|
|
||||||
from searx.utils import extract_text
|
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
|
||||||
from searx.extended_types import SXNG_Response
|
|
||||||
from searx.search.processors import OnlineParams
|
|
||||||
|
|
||||||
|
|
||||||
about = {
|
|
||||||
"website": "https://yandex.cloud/en/services/search-api",
|
|
||||||
"wikidata_id": "Q5281",
|
|
||||||
"official_api_documentation": "https://aistudio.yandex.ru/docs/en/search-api/api-ref/WebSearch/search.html",
|
|
||||||
"use_official_api": True,
|
|
||||||
"require_api_key": True,
|
|
||||||
"results": "XML",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Engine configuration
|
|
||||||
categories = ["general", "web"]
|
|
||||||
paging = True
|
|
||||||
safesearch = True
|
|
||||||
|
|
||||||
# Credentials, overwritten via settings.yml
|
|
||||||
api_key: str = ""
|
|
||||||
"""Yandex Cloud API key, passed as ``Authorization: Api-Key <api_key>``."""
|
|
||||||
|
|
||||||
yandex_folder_id: str = ""
|
|
||||||
"""Yandex Cloud folder id the API key belongs to."""
|
|
||||||
|
|
||||||
# Search tuning, overwritten via settings.yml
|
|
||||||
yandex_default_language: str = "en"
|
|
||||||
"""Default query language. It selects the Yandex search domain (e.g. yandex.ru
|
|
||||||
for ``ru``, yandex.com for ``en``) and the language of the search-result
|
|
||||||
notifications, but only as a fallback -- a request whose own locale matches
|
|
||||||
:py:obj:`language_map` overrides it. Must be one of its keys: ``ru``, ``be``,
|
|
||||||
``kk``, ``uk``, ``tr`` or ``en``."""
|
|
||||||
|
|
||||||
region: str = ""
|
|
||||||
"""Optional Yandex `region id`.
|
|
||||||
Only meaningful together with ``SEARCH_TYPE_RU``.
|
|
||||||
|
|
||||||
__ https://aistudio.yandex.ru/docs/en/search-api/reference/regions.html
|
|
||||||
"""
|
|
||||||
|
|
||||||
page_size: int = 10
|
|
||||||
"""Number of results requested per page."""
|
|
||||||
|
|
||||||
base_url = "https://searchapi.api.cloud.yandex.net/v2/web/search"
|
|
||||||
|
|
||||||
# searxng safesearch level -> Yandex familyMode
|
|
||||||
safesearch_map = {
|
|
||||||
0: "FAMILY_MODE_NONE",
|
|
||||||
1: "FAMILY_MODE_MODERATE",
|
|
||||||
2: "FAMILY_MODE_STRICT",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Map a query language to a (search_type, l10n) pair. It drives both the
|
|
||||||
# per-request override (when the query's locale matches) and the
|
|
||||||
# ``yandex_default_language`` default.
|
|
||||||
language_map = {
|
|
||||||
"ru": ("SEARCH_TYPE_RU", "LOCALIZATION_RU"),
|
|
||||||
"be": ("SEARCH_TYPE_BE", "LOCALIZATION_BE"),
|
|
||||||
"kk": ("SEARCH_TYPE_KK", "LOCALIZATION_KK"),
|
|
||||||
"uk": ("SEARCH_TYPE_RU", "LOCALIZATION_UK"),
|
|
||||||
"tr": ("SEARCH_TYPE_TR", "LOCALIZATION_TR"),
|
|
||||||
"en": ("SEARCH_TYPE_COM", "LOCALIZATION_EN"),
|
|
||||||
# Uzbek ('uz') is intentionally omitted: Yandex offers SEARCH_TYPE_UZ but no
|
|
||||||
# matching LOCALIZATION_UZ, so such queries fall back to the defaults above.
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def setup(_):
|
|
||||||
"""Validate credentials and paging limits when the engine is loaded."""
|
|
||||||
if not api_key or not yandex_folder_id:
|
|
||||||
raise SearxEngineAPIException("missing 'api_key' and/or 'yandex_folder_id' in engine settings")
|
|
||||||
if not 1 <= page_size <= 100:
|
|
||||||
raise SearxEngineAPIException("'page_size' must be in the range 1..100 (Yandex 'groupsOnPage')")
|
|
||||||
if yandex_default_language not in language_map:
|
|
||||||
raise SearxEngineAPIException(f"'yandex_default_language' must be one of {sorted(language_map)}")
|
|
||||||
|
|
||||||
|
|
||||||
def request(query: str, params: "OnlineParams"):
|
|
||||||
|
|
||||||
# Yandex returns at most 250 results for a query.
|
|
||||||
max_page = math.ceil(250 / page_size)
|
|
||||||
if params["pageno"] > max_page:
|
|
||||||
params["url"] = None
|
|
||||||
return
|
|
||||||
|
|
||||||
if len(query) > 400:
|
|
||||||
# Yandex rejects a 'queryText' longer than 400 characters; decline the
|
|
||||||
# request gracefully instead of provoking an API error.
|
|
||||||
params["url"] = None
|
|
||||||
return
|
|
||||||
|
|
||||||
lang = params["searxng_locale"].split("-")[0].lower()
|
|
||||||
req_search_type, req_l10n = language_map.get(lang, language_map[yandex_default_language])
|
|
||||||
|
|
||||||
body: dict[str, t.Any] = {
|
|
||||||
"query": {
|
|
||||||
"searchType": req_search_type,
|
|
||||||
"queryText": query,
|
|
||||||
"familyMode": safesearch_map[params["safesearch"]],
|
|
||||||
# the API uses a 0-based page index
|
|
||||||
"page": str(params["pageno"] - 1),
|
|
||||||
},
|
|
||||||
"groupSpec": {
|
|
||||||
"groupMode": "GROUP_MODE_FLAT",
|
|
||||||
"groupsOnPage": str(page_size),
|
|
||||||
"docsInGroup": "1",
|
|
||||||
},
|
|
||||||
"l10n": req_l10n,
|
|
||||||
"folderId": yandex_folder_id,
|
|
||||||
"responseFormat": "FORMAT_XML",
|
|
||||||
}
|
|
||||||
# Yandex accepts a 'region' only together with the Russian search type.
|
|
||||||
if region and req_search_type == "SEARCH_TYPE_RU":
|
|
||||||
body["region"] = region
|
|
||||||
|
|
||||||
params["method"] = "POST"
|
|
||||||
params["url"] = base_url
|
|
||||||
params["headers"]["Authorization"] = f"Api-Key {api_key}"
|
|
||||||
params["headers"]["Content-Type"] = "application/json"
|
|
||||||
params["json"] = body
|
|
||||||
|
|
||||||
|
|
||||||
def _raw_xml(resp: "SXNG_Response") -> bytes:
|
|
||||||
"""Extract and Base64-decode the XML payload out of the JSON envelope.
|
|
||||||
|
|
||||||
The synchronous ``/v2/web/search`` endpoint returns ``{"rawData":
|
|
||||||
"<base64>"}`` on success; HTTP errors are raised upstream via
|
|
||||||
``raise_for_httperror``.
|
|
||||||
"""
|
|
||||||
data: dict[str, t.Any] = resp.json()
|
|
||||||
|
|
||||||
raw_data = data.get("rawData")
|
|
||||||
if raw_data is None:
|
|
||||||
raise SearxEngineAPIException("Yandex Search API: no 'rawData' in response")
|
|
||||||
|
|
||||||
return b64decode(raw_data)
|
|
||||||
|
|
||||||
|
|
||||||
def response(resp: "SXNG_Response") -> EngineResults:
|
|
||||||
res = EngineResults()
|
|
||||||
|
|
||||||
dom = etree.fromstring(_raw_xml(resp)) # pylint: disable=c-extension-no-member
|
|
||||||
|
|
||||||
# An <error> inside <response> signals an application error. Code 15 simply
|
|
||||||
# means "nothing was found" and must not raise.
|
|
||||||
error = dom.find(".//response/error")
|
|
||||||
if error is not None:
|
|
||||||
if error.get("code") == "15":
|
|
||||||
return res
|
|
||||||
raise SearxEngineAPIException(f"Yandex Search API error {error.get('code')}: {error.text}")
|
|
||||||
|
|
||||||
for doc in dom.iterfind(".//doc"):
|
|
||||||
url = extract_text(doc.find("url"), allow_none=True)
|
|
||||||
title = extract_text(doc.find("title"), allow_none=True)
|
|
||||||
if not url or not title:
|
|
||||||
continue
|
|
||||||
|
|
||||||
content = extract_text(doc.find("headline"), allow_none=True)
|
|
||||||
if not content:
|
|
||||||
passages = doc.find("passages")
|
|
||||||
if passages is not None:
|
|
||||||
content = " ".join(extract_text(p) or "" for p in passages.iterfind("passage")).strip()
|
|
||||||
|
|
||||||
res.add(
|
|
||||||
res.types.MainResult(
|
|
||||||
url=url,
|
|
||||||
title=title,
|
|
||||||
content=content or "",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return res
|
|
||||||
@@ -13,7 +13,6 @@ cfg_schema = 1 # config's schema version no.
|
|||||||
# "duckduckgo" = "searx.favicons.resolvers.duckduckgo"
|
# "duckduckgo" = "searx.favicons.resolvers.duckduckgo"
|
||||||
# "allesedv" = "searx.favicons.resolvers.allesedv"
|
# "allesedv" = "searx.favicons.resolvers.allesedv"
|
||||||
# "google" = "searx.favicons.resolvers.google"
|
# "google" = "searx.favicons.resolvers.google"
|
||||||
# "kagi" = "searx.favicons.resolvers.kagi"
|
|
||||||
# "yandex" = "searx.favicons.resolvers.yandex"
|
# "yandex" = "searx.favicons.resolvers.yandex"
|
||||||
|
|
||||||
[favicons.cache]
|
[favicons.cache]
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ timeout``) and returns a tuple ``(data, mime)``.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["DEFAULT_RESOLVER_MAP", "allesedv", "duckduckgo", "google", "kagi", "yandex"]
|
__all__ = ["DEFAULT_RESOLVER_MAP", "allesedv", "duckduckgo", "google", "yandex"]
|
||||||
|
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from searx import network
|
from searx import network
|
||||||
@@ -76,21 +76,6 @@ def google(domain: str, timeout: int) -> tuple[None | bytes, None | str]:
|
|||||||
return data, mime
|
return data, mime
|
||||||
|
|
||||||
|
|
||||||
def kagi(domain: str, timeout: int) -> tuple[None | bytes, None | str]:
|
|
||||||
"""Favicon Resolver from kagi.com / https://news.kagi.com/api/favicon-proxy"""
|
|
||||||
data, mime = (None, None)
|
|
||||||
# quality can be either 'fast' or 'best', best is slower and can return svgs (e.g. github.com)
|
|
||||||
url = f"https://news.kagi.com/api/favicon-proxy?domain={domain}&quality=fast"
|
|
||||||
logger.debug("fetch favicon from: %s", url)
|
|
||||||
|
|
||||||
# will return a 404 if the favicon does not exist and a 200 if it does,
|
|
||||||
response = network.get(url, **_req_args(timeout=timeout))
|
|
||||||
if response and response.status_code == 200:
|
|
||||||
mime = response.headers['Content-Type']
|
|
||||||
data = response.content
|
|
||||||
return data, mime
|
|
||||||
|
|
||||||
|
|
||||||
def yandex(domain: str, timeout: int) -> tuple[None | bytes, None | str]:
|
def yandex(domain: str, timeout: int) -> tuple[None | bytes, None | str]:
|
||||||
"""Favicon Resolver from yandex.com"""
|
"""Favicon Resolver from yandex.com"""
|
||||||
data, mime = (None, None)
|
data, mime = (None, None)
|
||||||
@@ -110,6 +95,5 @@ DEFAULT_RESOLVER_MAP = {
|
|||||||
"allesedv": allesedv,
|
"allesedv": allesedv,
|
||||||
"duckduckgo": duckduckgo,
|
"duckduckgo": duckduckgo,
|
||||||
"google": google,
|
"google": google,
|
||||||
"kagi": kagi,
|
|
||||||
"yandex": yandex,
|
"yandex": yandex,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class PluginInfo:
|
|||||||
description: str
|
description: str
|
||||||
"""Short description of the *answerer*."""
|
"""Short description of the *answerer*."""
|
||||||
|
|
||||||
preference_section: t.Literal["general", "ui", "privacy", "query"] | None = "general"
|
preference_section: t.Literal["general", "ui", "privacy", "query", "ai"] | None = "general"
|
||||||
"""Section (tab/group) in the preferences where this plugin is shown to the
|
"""Section (tab/group) in the preferences where this plugin is shown to the
|
||||||
user.
|
user.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,386 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
"""Implementation of the AI Summary plugin, which shows a generated answer above
|
||||||
|
the search results. The answer comes from an LLM server that implements the
|
||||||
|
`OpenAI chat completions API`_ (Ollama, vLLM, llama.cpp, LM Studio, Hugging Face
|
||||||
|
TGI, ...) and that the administrator runs.
|
||||||
|
|
||||||
|
- :ref:`ai_summary plugin` describes the design and the request flow.
|
||||||
|
- :ref:`settings ai_summary` describes how to configure it.
|
||||||
|
|
||||||
|
This module holds the plugin itself and the ``/ai_summary`` endpoint
|
||||||
|
(:py:obj:`ai_summary_view`, registered in :py:obj:`searx.webapp`). The endpoint
|
||||||
|
streams the answer to the browser, so that the result page is never delayed by
|
||||||
|
the LLM; :py:obj:`SXNGPlugin.post_search` only adds an empty
|
||||||
|
:py:obj:`searx.result_types.AiSummary` placeholder for the client to fill.
|
||||||
|
|
||||||
|
Settings of the ``ai_summary:`` section are defined in
|
||||||
|
:py:obj:`searx.ai_summary.SettingsAISummary`.
|
||||||
|
|
||||||
|
.. _OpenAI chat completions API: https://platform.openai.com/docs/api-reference/chat
|
||||||
|
"""
|
||||||
|
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import flask
|
||||||
|
import httpx
|
||||||
|
from flask_babel import gettext
|
||||||
|
|
||||||
|
from searx import get_setting
|
||||||
|
from searx.ai_summary import SettingsAISummary, build_chat_messages
|
||||||
|
from searx.extended_types import sxng_request
|
||||||
|
from searx.result_types import EngineResults
|
||||||
|
import searx.ai_summary
|
||||||
|
|
||||||
|
from . import Plugin, PluginInfo
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
from searx.search import SearchWithPlugins
|
||||||
|
from searx.extended_types import SXNG_Request
|
||||||
|
from . import PluginCfg
|
||||||
|
|
||||||
|
VALID_ROLES = ("user", "assistant")
|
||||||
|
# Model names differ per provider: "gemma3:4b" (Ollama), "bedrock/anthropic.
|
||||||
|
# claude-3-5-sonnet" (a gateway's routing prefix), "gemini-1.5-pro@001" (a
|
||||||
|
# pinned version). The pattern accepts those and rejects anything that could
|
||||||
|
# change the meaning of the request body it is placed into.
|
||||||
|
MODEL_NAME_REGEXP = re.compile(r"[A-Za-z0-9._:/@-]{1,128}")
|
||||||
|
|
||||||
|
log = logging.getLogger("searx.plugins.ai_summary")
|
||||||
|
|
||||||
|
UPSTREAM_RETRIES = 1
|
||||||
|
"""How often a request to the LLM server is repeated when the server does not
|
||||||
|
answer in time.
|
||||||
|
|
||||||
|
An idle LLM server unloads the model, and loads it again on the next request --
|
||||||
|
which can take longer than :py:obj:`read_timeout
|
||||||
|
<searx.ai_summary.SettingsAISummary.read_timeout>`, because no byte of the
|
||||||
|
response is sent while the model is loading. The request that runs into this
|
||||||
|
is also the request that starts the load, so repeating it usually succeeds."""
|
||||||
|
|
||||||
|
|
||||||
|
def _get_client(base_url: str, cfg: SettingsAISummary, api_key: str = "") -> httpx.Client:
|
||||||
|
"""HTTP client for one request to the LLM server at ``base_url``. The
|
||||||
|
``api_key`` (if any) is sent in an ``Authorization: Bearer`` header, see
|
||||||
|
:py:obj:`_server_api_key`."""
|
||||||
|
# the OpenAI API paths are prefixed with /v1, unless the base URL already
|
||||||
|
# points into an API prefix
|
||||||
|
base_url = base_url.rstrip("/")
|
||||||
|
if not base_url.endswith("/v1"):
|
||||||
|
base_url += "/v1"
|
||||||
|
return httpx.Client(
|
||||||
|
base_url=base_url,
|
||||||
|
headers={"Authorization": f"Bearer {api_key}"} if api_key else None,
|
||||||
|
timeout=httpx.Timeout(connect=cfg.connect_timeout, read=cfg.read_timeout, write=10.0, pool=10.0),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _valid_server(url: str) -> bool:
|
||||||
|
try:
|
||||||
|
parsed = urlparse(url)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
return parsed.scheme in ("http", "https") and bool(parsed.netloc) and len(url) <= 256
|
||||||
|
|
||||||
|
|
||||||
|
def _server_id(url: str) -> tuple[str, str, int, str] | None:
|
||||||
|
"""Identity of an LLM server URL (scheme, host, port, path) for comparing
|
||||||
|
two URLs, or ``None`` if the URL is unusable. The ``/v1`` API prefix is
|
||||||
|
not part of the identity, :py:obj:`_get_client` appends it when missing."""
|
||||||
|
try:
|
||||||
|
parsed = urlparse(url)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
# .hostname (not .netloc) drops the userinfo, so that a server URL like
|
||||||
|
# http://llm.example.org@untrusted.example.org/ is identified by the host
|
||||||
|
# the request is actually sent to (untrusted.example.org)
|
||||||
|
if parsed.scheme not in ("http", "https") or not parsed.hostname:
|
||||||
|
return None
|
||||||
|
port = parsed.port or (443 if parsed.scheme == "https" else 80)
|
||||||
|
path = parsed.path.rstrip("/")
|
||||||
|
if path.endswith("/v1"):
|
||||||
|
path = path[: -len("/v1")].rstrip("/")
|
||||||
|
return (parsed.scheme, parsed.hostname.lower(), port, path)
|
||||||
|
|
||||||
|
|
||||||
|
def _server_api_key(cfg: SettingsAISummary, server: str, user_api_key: str = "") -> str:
|
||||||
|
"""The API key to send to ``server``:
|
||||||
|
|
||||||
|
- the administrator's :py:obj:`cfg.api_key
|
||||||
|
<searx.ai_summary.SettingsAISummary.api_key>` if ``server`` *is* the
|
||||||
|
administrator's server (:py:obj:`cfg.base_url
|
||||||
|
<searx.ai_summary.SettingsAISummary.base_url>`),
|
||||||
|
- otherwise the user's own ``ai_summary_api_key`` preference, which belongs
|
||||||
|
to the server in the user's own ``ai_summary_server`` preference.
|
||||||
|
|
||||||
|
The administrator's key is never sent to a server a user configured --
|
||||||
|
that would hand every user of the instance a way to capture it."""
|
||||||
|
server_id = _server_id(server)
|
||||||
|
if server_id is not None and server_id == _server_id(cfg.base_url):
|
||||||
|
return cfg.api_key
|
||||||
|
return user_api_key
|
||||||
|
|
||||||
|
|
||||||
|
def _user_server(request: "SXNG_Request", cfg: SettingsAISummary) -> str:
|
||||||
|
"""The LLM server URL for this request: the user's ``ai_summary_server``
|
||||||
|
preference, or the administrator's default."""
|
||||||
|
server = str(request.preferences.get_value("ai_summary_server") or "").strip()
|
||||||
|
# Credentials are stripped from a user's server URL: httpx turns them into
|
||||||
|
# an Authorization header, and a user should not be able to make SearXNG
|
||||||
|
# send a header of their choosing to a host of their choosing. An
|
||||||
|
# administrator can still use credentials in the configured base_url (e.g.
|
||||||
|
# an LLM server behind basic auth).
|
||||||
|
if server and "@" in urlparse(server).netloc:
|
||||||
|
server = ""
|
||||||
|
return server or cfg.base_url
|
||||||
|
|
||||||
|
|
||||||
|
class SXNGPlugin(Plugin):
|
||||||
|
"""Plugin that adds the AI summary placeholder to the result page, the
|
||||||
|
``/ai_summary`` endpoint itself is registered in :py:obj:`searx.webapp`."""
|
||||||
|
|
||||||
|
id = "ai_summary"
|
||||||
|
|
||||||
|
def __init__(self, plg_cfg: "PluginCfg"):
|
||||||
|
super().__init__(plg_cfg)
|
||||||
|
|
||||||
|
self.info = PluginInfo(
|
||||||
|
id=self.id,
|
||||||
|
name=gettext("AI Summary"),
|
||||||
|
description=gettext(
|
||||||
|
"Show an AI generated summary of the search query on top of the"
|
||||||
|
" result page (uses a local LLM server, see the settings below)."
|
||||||
|
),
|
||||||
|
preference_section="ai",
|
||||||
|
)
|
||||||
|
|
||||||
|
def init(self, app: "flask.Flask") -> bool:
|
||||||
|
cfg: SettingsAISummary = get_setting("ai_summary")
|
||||||
|
|
||||||
|
if cfg.base_url:
|
||||||
|
searx.ai_summary.MODELS = list(cfg.models) or self._probe_models(cfg)
|
||||||
|
if not cfg.model and searx.ai_summary.MODELS:
|
||||||
|
cfg.model = searx.ai_summary.MODELS[0]
|
||||||
|
if cfg.model and cfg.model not in searx.ai_summary.MODELS:
|
||||||
|
searx.ai_summary.MODELS.insert(0, cfg.model)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _probe_models(self, cfg: SettingsAISummary) -> list[str]:
|
||||||
|
"""Request the list of models from the LLM server (``GET
|
||||||
|
/v1/models``). The server might not be up when SearXNG starts, a
|
||||||
|
failing probe only leaves the model suggestion list empty."""
|
||||||
|
try:
|
||||||
|
with _get_client(cfg.base_url, cfg, cfg.api_key) as client:
|
||||||
|
resp = client.get("/models")
|
||||||
|
resp.raise_for_status()
|
||||||
|
models = [model["id"] for model in resp.json().get("data", [])]
|
||||||
|
except (httpx.HTTPError, ValueError, KeyError) as exc:
|
||||||
|
self.log.warning("can't request model list from %s: %s", cfg.base_url, exc)
|
||||||
|
models = []
|
||||||
|
return models or ([cfg.model] if cfg.model else [])
|
||||||
|
|
||||||
|
def post_search(self, request: "SXNG_Request", search: "SearchWithPlugins") -> EngineResults | None:
|
||||||
|
results = EngineResults()
|
||||||
|
sq = search.search_query
|
||||||
|
cfg: SettingsAISummary = get_setting("ai_summary")
|
||||||
|
|
||||||
|
skip = (
|
||||||
|
sq.pageno > 1
|
||||||
|
# post_search is also called for the json, csv and rss formats,
|
||||||
|
# the placeholder is only useful on the HTML result page
|
||||||
|
or request.form.get("format", "html") != "html"
|
||||||
|
or "general" not in sq.categories
|
||||||
|
# an infobox (e.g. wikipedia / wikidata) or an instant answer
|
||||||
|
# (e.g. ddg definitions) most likely already answers the query
|
||||||
|
or bool(search.result_container.infoboxes)
|
||||||
|
or bool(search.result_container.answers)
|
||||||
|
or not sq.query.strip()
|
||||||
|
# without an LLM server (user preference or admin default)
|
||||||
|
# there is nothing to show
|
||||||
|
or not _user_server(request, cfg)
|
||||||
|
)
|
||||||
|
if skip:
|
||||||
|
return None
|
||||||
|
|
||||||
|
grounding = bool(request.preferences.get_value("ai_summary_grounding"))
|
||||||
|
results.add(results.types.AiSummary(query=sq.query, grounding=grounding))
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def _bad_request(msg: str) -> flask.Response:
|
||||||
|
return flask.Response(json.dumps({"error": msg}), status=400, mimetype="application/json")
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_messages(messages: t.Any, cfg: SettingsAISummary) -> list[dict[str, str]]:
|
||||||
|
if not isinstance(messages, list) or not messages or len(messages) > cfg.max_history_messages:
|
||||||
|
raise ValueError("invalid messages")
|
||||||
|
for msg in messages:
|
||||||
|
if not isinstance(msg, dict) or msg.keys() != {"role", "content"}:
|
||||||
|
raise ValueError("invalid message")
|
||||||
|
if msg["role"] not in VALID_ROLES or not isinstance(msg["content"], str):
|
||||||
|
raise ValueError("invalid message")
|
||||||
|
if not msg["content"].strip() or len(msg["content"]) > cfg.max_message_length:
|
||||||
|
raise ValueError("invalid message")
|
||||||
|
if messages[-1]["role"] != "user":
|
||||||
|
raise ValueError("last message is not a user message")
|
||||||
|
return messages
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_context(context: t.Any, cfg: SettingsAISummary) -> list[dict[str, str]]:
|
||||||
|
if not isinstance(context, list) or len(context) > cfg.max_context_items:
|
||||||
|
raise ValueError("invalid context")
|
||||||
|
for item in context:
|
||||||
|
if not isinstance(item, dict) or not item.keys() <= {"title", "url", "snippet"}:
|
||||||
|
raise ValueError("invalid context item")
|
||||||
|
for val in item.values():
|
||||||
|
if not isinstance(val, str) or len(val) > cfg.max_message_length:
|
||||||
|
raise ValueError("invalid context item")
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_payload(payload: t.Any, cfg: SettingsAISummary) -> tuple[list[dict[str, str]], list[dict[str, str]]]:
|
||||||
|
"""Validate the request body of the ``/ai_summary`` endpoint and return
|
||||||
|
the ``messages`` and ``context`` lists. Raises a :py:obj:`ValueError` for
|
||||||
|
any malformed payload."""
|
||||||
|
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
raise ValueError("payload is not an object")
|
||||||
|
return _validate_messages(payload.get("messages"), cfg), _validate_context(payload.get("context", []), cfg)
|
||||||
|
|
||||||
|
|
||||||
|
def _open_upstream(client: httpx.Client, payload: dict[str, t.Any]) -> tuple[t.Any, t.Any]:
|
||||||
|
"""Start the streaming completion on the LLM server.
|
||||||
|
|
||||||
|
Returns the (already entered) stream context and the response, or
|
||||||
|
``(None, None)`` if no usable response was received.
|
||||||
|
|
||||||
|
The request is repeated (:py:obj:`UPSTREAM_RETRIES`) when the server did not
|
||||||
|
answer in time, and when it answered ``5xx`` -- both mean *not right now*,
|
||||||
|
and the most common reason is a model that is still being loaded. A ``4xx``
|
||||||
|
is not repeated: a wrong API key or an unknown model name does not become
|
||||||
|
right when asked twice."""
|
||||||
|
|
||||||
|
for attempt in range(UPSTREAM_RETRIES + 1):
|
||||||
|
stream_ctx = client.stream("POST", "/chat/completions", json=payload)
|
||||||
|
try:
|
||||||
|
resp = stream_ctx.__enter__() # pylint: disable=unnecessary-dunder-call
|
||||||
|
except httpx.TransportError as exc:
|
||||||
|
if attempt < UPSTREAM_RETRIES:
|
||||||
|
log.debug("LLM server did not answer (%s), asking again", exc)
|
||||||
|
continue
|
||||||
|
log.warning("LLM server did not answer: %s", exc)
|
||||||
|
return None, None
|
||||||
|
except httpx.HTTPError as exc:
|
||||||
|
log.warning("request to the LLM server failed: %s", exc)
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
if resp.status_code == 200:
|
||||||
|
return stream_ctx, resp
|
||||||
|
|
||||||
|
# the body of an error response is short and usually names the cause,
|
||||||
|
# e.g. an unknown model; without it a misconfiguration is invisible
|
||||||
|
detail = ""
|
||||||
|
try:
|
||||||
|
resp.read()
|
||||||
|
detail = resp.text.strip()[:200]
|
||||||
|
except (httpx.HTTPError, UnicodeDecodeError): # pragma: no cover
|
||||||
|
pass
|
||||||
|
stream_ctx.__exit__(None, None, None)
|
||||||
|
|
||||||
|
# 5xx is the server saying it is not able to answer *right now* -- a
|
||||||
|
# model still loading, a gateway with no upstream yet. 4xx is the
|
||||||
|
# server saying the request is wrong, which a second one would be too.
|
||||||
|
if resp.status_code >= 500 and attempt < UPSTREAM_RETRIES:
|
||||||
|
log.debug("LLM server replied HTTP %s (%s), asking again", resp.status_code, detail)
|
||||||
|
continue
|
||||||
|
log.warning("LLM server responded with HTTP %s %s", resp.status_code, detail)
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
return None, None # pragma: no cover - the loop always returns
|
||||||
|
|
||||||
|
|
||||||
|
def ai_summary_view() -> flask.Response:
|
||||||
|
"""Stream an AI generated answer for the messages in the request body,
|
||||||
|
response is NDJSON: ``{"delta": ..}`` lines followed by one final
|
||||||
|
``{"done": true, ..}`` line."""
|
||||||
|
|
||||||
|
cfg: SettingsAISummary = get_setting("ai_summary")
|
||||||
|
|
||||||
|
if SXNGPlugin.id not in sxng_request.user_plugins:
|
||||||
|
return flask.Response(json.dumps({"error": "plugin is not enabled"}), status=403, mimetype="application/json")
|
||||||
|
|
||||||
|
try:
|
||||||
|
messages, context = _validate_payload(sxng_request.get_json(force=True, silent=True), cfg)
|
||||||
|
except ValueError as exc:
|
||||||
|
return _bad_request(str(exc))
|
||||||
|
|
||||||
|
server = _user_server(sxng_request, cfg)
|
||||||
|
if not _valid_server(server):
|
||||||
|
return _bad_request("no valid LLM server configured")
|
||||||
|
|
||||||
|
model = str(sxng_request.preferences.get_value("ai_summary_model") or "").strip() or cfg.model
|
||||||
|
if not MODEL_NAME_REGEXP.fullmatch(model):
|
||||||
|
return _bad_request("no valid model configured")
|
||||||
|
|
||||||
|
chat_payload = {
|
||||||
|
"model": model,
|
||||||
|
"messages": build_chat_messages(cfg, messages, context),
|
||||||
|
"stream": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
# open the upstream connection before streaming, a connection error is
|
||||||
|
# reported as HTTP 502 instead of a line in an already started stream
|
||||||
|
user_api_key = str(sxng_request.preferences.get_value("ai_summary_api_key") or "").strip()
|
||||||
|
client = _get_client(server, cfg, _server_api_key(cfg, server, user_api_key))
|
||||||
|
stream_ctx, upstream = _open_upstream(client, chat_payload)
|
||||||
|
if stream_ctx is None or upstream is None:
|
||||||
|
client.close()
|
||||||
|
return flask.Response(json.dumps({"error": "upstream error"}), status=502, mimetype="application/json")
|
||||||
|
|
||||||
|
# from here on nothing must be read from the request context, the
|
||||||
|
# generator runs after the request context has been torn down
|
||||||
|
|
||||||
|
def ndjson(obj: dict[str, t.Any]) -> bytes:
|
||||||
|
# the generator bypasses flask's response encoding (direct_passthrough)
|
||||||
|
return (json.dumps(obj) + "\n").encode()
|
||||||
|
|
||||||
|
def generate():
|
||||||
|
start = time.monotonic()
|
||||||
|
try:
|
||||||
|
# the upstream is a SSE stream: "data: {..}" lines, terminated by
|
||||||
|
# a "data: [DONE]" line
|
||||||
|
for line in upstream.iter_lines():
|
||||||
|
if time.monotonic() - start > cfg.stream_timeout:
|
||||||
|
yield ndjson({"done": True, "error": "timeout"})
|
||||||
|
return
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith(":") or not line.startswith("data:"):
|
||||||
|
continue
|
||||||
|
payload = line[len("data:") :].strip()
|
||||||
|
if payload == "[DONE]":
|
||||||
|
break
|
||||||
|
data = json.loads(payload)
|
||||||
|
choices = data.get("choices") or [{}]
|
||||||
|
delta = choices[0].get("delta", {}).get("content") or ""
|
||||||
|
if delta:
|
||||||
|
yield ndjson({"delta": delta})
|
||||||
|
yield ndjson({"done": True, "model": model})
|
||||||
|
except (httpx.HTTPError, ValueError) as exc:
|
||||||
|
log.warning("error while streaming from the LLM server: %s", exc)
|
||||||
|
yield ndjson({"done": True, "error": "upstream error"})
|
||||||
|
finally:
|
||||||
|
stream_ctx.__exit__(None, None, None)
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
return flask.Response(
|
||||||
|
generate(),
|
||||||
|
mimetype="application/x-ndjson",
|
||||||
|
headers={"Cache-Control": "no-store", "X-Accel-Buffering": "no"},
|
||||||
|
direct_passthrough=True,
|
||||||
|
)
|
||||||
+27
-2
@@ -49,10 +49,14 @@ class ValidationException(Exception):
|
|||||||
class Setting:
|
class Setting:
|
||||||
"""Base class of user settings"""
|
"""Base class of user settings"""
|
||||||
|
|
||||||
def __init__(self, default_value: t.Any, locked: bool = False):
|
def __init__(self, default_value: t.Any, locked: bool = False, secret: bool = False):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.value: t.Any = default_value
|
self.value: t.Any = default_value
|
||||||
self.locked: bool = locked
|
self.locked: bool = locked
|
||||||
|
self.secret: bool = secret
|
||||||
|
"""The value is a credential: it is not included in the preferences URL
|
||||||
|
(:py:obj:`Preferences.get_as_url_params`), which users copy around to
|
||||||
|
transfer or share their preferences."""
|
||||||
|
|
||||||
def parse(self, data: str):
|
def parse(self, data: str):
|
||||||
"""Parse ``data`` and store the result at ``self.value``
|
"""Parse ``data`` and store the result at ``self.value``
|
||||||
@@ -462,6 +466,27 @@ class Preferences:
|
|||||||
locked="doi_resolver" in self.cfg.lock,
|
locked="doi_resolver" in self.cfg.lock,
|
||||||
choices=DOI_RESOLVERS,
|
choices=DOI_RESOLVERS,
|
||||||
),
|
),
|
||||||
|
# empty values fall back to the administrator's defaults in the
|
||||||
|
# ai_summary: section (searx.ai_summary.SettingsAISummary)
|
||||||
|
'ai_summary_server': StringSetting(
|
||||||
|
"",
|
||||||
|
locked="ai_summary_server" in self.cfg.lock,
|
||||||
|
),
|
||||||
|
'ai_summary_api_key': StringSetting(
|
||||||
|
"",
|
||||||
|
locked="ai_summary_api_key" in self.cfg.lock,
|
||||||
|
# a user's API key is only sent to a server the user configured
|
||||||
|
# themselves, and it is never part of the preferences URL
|
||||||
|
secret=True,
|
||||||
|
),
|
||||||
|
'ai_summary_model': StringSetting(
|
||||||
|
"",
|
||||||
|
locked="ai_summary_model" in self.cfg.lock,
|
||||||
|
),
|
||||||
|
'ai_summary_grounding': BooleanSetting(
|
||||||
|
get_setting("ai_summary").grounding,
|
||||||
|
locked="ai_summary_grounding" in self.cfg.lock,
|
||||||
|
),
|
||||||
'simple_style': EnumStringSetting(
|
'simple_style': EnumStringSetting(
|
||||||
get_setting("ui.theme_args.simple_style"),
|
get_setting("ui.theme_args.simple_style"),
|
||||||
locked="simple_style" in self.cfg.lock,
|
locked="simple_style" in self.cfg.lock,
|
||||||
@@ -498,7 +523,7 @@ class Preferences:
|
|||||||
"""Return preferences as URL parameters"""
|
"""Return preferences as URL parameters"""
|
||||||
settings_kv = {}
|
settings_kv = {}
|
||||||
for k, v in self.key_value_settings.items():
|
for k, v in self.key_value_settings.items():
|
||||||
if v.locked:
|
if v.locked or v.secret:
|
||||||
continue
|
continue
|
||||||
if isinstance(v, MultipleChoiceSetting):
|
if isinstance(v, MultipleChoiceSetting):
|
||||||
settings_kv[k] = ','.join(v.get_value())
|
settings_kv[k] = ','.join(v.get_value())
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ __all__ = [
|
|||||||
"EngineResults",
|
"EngineResults",
|
||||||
"AnswerSet",
|
"AnswerSet",
|
||||||
"Answer",
|
"Answer",
|
||||||
|
"AiSummary",
|
||||||
"Translations",
|
"Translations",
|
||||||
"WeatherAnswer",
|
"WeatherAnswer",
|
||||||
"Code",
|
"Code",
|
||||||
@@ -32,7 +33,7 @@ import typing as t
|
|||||||
import abc
|
import abc
|
||||||
|
|
||||||
from ._base import Result, MainResult, LegacyResult
|
from ._base import Result, MainResult, LegacyResult
|
||||||
from .answer import AnswerSet, Answer, Translations, WeatherAnswer
|
from .answer import AnswerSet, Answer, AiSummary, Translations, WeatherAnswer
|
||||||
from .keyvalue import KeyValue
|
from .keyvalue import KeyValue
|
||||||
from .code import Code
|
from .code import Code
|
||||||
from .paper import Paper
|
from .paper import Paper
|
||||||
@@ -49,6 +50,7 @@ class ResultList(list[Result | LegacyResult], abc.ABC):
|
|||||||
implemented)."""
|
implemented)."""
|
||||||
|
|
||||||
Answer = Answer
|
Answer = Answer
|
||||||
|
AiSummary = AiSummary
|
||||||
KeyValue = KeyValue
|
KeyValue = KeyValue
|
||||||
Code = Code
|
Code = Code
|
||||||
Paper = Paper
|
Paper = Paper
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ template.
|
|||||||
:members:
|
:members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. autoclass:: AiSummary
|
||||||
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
.. autoclass:: Translations
|
.. autoclass:: Translations
|
||||||
:members:
|
:members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
@@ -29,7 +33,7 @@ template.
|
|||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["AnswerSet", "Answer", "Translations", "WeatherAnswer"]
|
__all__ = ["AnswerSet", "Answer", "AiSummary", "Translations", "WeatherAnswer"]
|
||||||
|
|
||||||
from flask_babel import gettext
|
from flask_babel import gettext
|
||||||
import msgspec
|
import msgspec
|
||||||
@@ -92,6 +96,28 @@ class Answer(BaseAnswer, kw_only=True):
|
|||||||
return hash(self.answer)
|
return hash(self.answer)
|
||||||
|
|
||||||
|
|
||||||
|
class AiSummary(BaseAnswer, kw_only=True):
|
||||||
|
"""Placeholder for an AI generated summary of the search query
|
||||||
|
(:py:obj:`searx.plugins.ai_summary`). The summary itself is fetched
|
||||||
|
asynchronously by the client after the result page has been rendered."""
|
||||||
|
|
||||||
|
# The answers of an AnswerSet are sorted by their template name; this
|
||||||
|
# template sorts before the other answer templates, the AI summary is
|
||||||
|
# therefore rendered first in the answer area.
|
||||||
|
template: str = "answer/ai_summary.html"
|
||||||
|
|
||||||
|
query: str
|
||||||
|
"""The search query the summary is generated for."""
|
||||||
|
|
||||||
|
grounding: bool = False
|
||||||
|
"""Ground the summary on the search results (user's preference)."""
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
"""The hash value of field *query* is the hash value of the
|
||||||
|
:py:obj:`AiSummary` object."""
|
||||||
|
return hash(self.query)
|
||||||
|
|
||||||
|
|
||||||
class Translations(BaseAnswer, kw_only=True):
|
class Translations(BaseAnswer, kw_only=True):
|
||||||
"""Answer type with a list of translations.
|
"""Answer type with a list of translations.
|
||||||
|
|
||||||
|
|||||||
@@ -255,11 +255,11 @@ class OnlineProcessor(EngineProcessor):
|
|||||||
except ssl.SSLError as e:
|
except ssl.SSLError as e:
|
||||||
# requests timeout (connect or read)
|
# requests timeout (connect or read)
|
||||||
self.handle_exception(result_container, e, suspend=True)
|
self.handle_exception(result_container, e, suspend=True)
|
||||||
self.logger.debug("SSLError {}, verify={}".format(e, searx.network.get_network(self.engine.name).verify))
|
self.logger.error("SSLError {}, verify={}".format(e, searx.network.get_network(self.engine.name).verify))
|
||||||
except (httpx.TimeoutException, asyncio.TimeoutError) as e:
|
except (httpx.TimeoutException, asyncio.TimeoutError) as e:
|
||||||
# requests timeout (connect or read)
|
# requests timeout (connect or read)
|
||||||
self.handle_exception(result_container, e, suspend=True)
|
self.handle_exception(result_container, e, suspend=True)
|
||||||
self.logger.debug(
|
self.logger.error(
|
||||||
"HTTP requests timeout (search duration : {0} s, timeout: {1} s) : {2}".format(
|
"HTTP requests timeout (search duration : {0} s, timeout: {1} s) : {2}".format(
|
||||||
default_timer() - start_time, timeout_limit, e.__class__.__name__
|
default_timer() - start_time, timeout_limit, e.__class__.__name__
|
||||||
)
|
)
|
||||||
@@ -267,7 +267,7 @@ class OnlineProcessor(EngineProcessor):
|
|||||||
except (httpx.HTTPError, httpx.StreamError) as e:
|
except (httpx.HTTPError, httpx.StreamError) as e:
|
||||||
# other requests exception
|
# other requests exception
|
||||||
self.handle_exception(result_container, e, suspend=True)
|
self.handle_exception(result_container, e, suspend=True)
|
||||||
self.logger.debug(
|
self.logger.exception(
|
||||||
"requests exception (search duration : {0} s, timeout: {1} s) : {2}".format(
|
"requests exception (search duration : {0} s, timeout: {1} s) : {2}".format(
|
||||||
default_timer() - start_time, timeout_limit, e
|
default_timer() - start_time, timeout_limit, e
|
||||||
)
|
)
|
||||||
@@ -278,7 +278,7 @@ class OnlineProcessor(EngineProcessor):
|
|||||||
SearxEngineAccessDeniedException,
|
SearxEngineAccessDeniedException,
|
||||||
) as e:
|
) as e:
|
||||||
self.handle_exception(result_container, e, suspend=True)
|
self.handle_exception(result_container, e, suspend=True)
|
||||||
self.logger.debug(e.message)
|
self.logger.exception(e.message)
|
||||||
except Exception as e: # pylint: disable=broad-except
|
except Exception as e: # pylint: disable=broad-except
|
||||||
self.handle_exception(result_container, e)
|
self.handle_exception(result_container, e)
|
||||||
self.logger.debug("exception : {0}".format(e))
|
self.logger.exception("exception : {0}".format(e))
|
||||||
|
|||||||
+82
-39
@@ -42,12 +42,12 @@ search:
|
|||||||
safe_search: 0
|
safe_search: 0
|
||||||
# Existing autocomplete backends: "360search", "baidu", "bing", "brave", "dbpedia", "duckduckgo", "google",
|
# Existing autocomplete backends: "360search", "baidu", "bing", "brave", "dbpedia", "duckduckgo", "google",
|
||||||
# "yandex", "privacywall", "mwmbl", "naver", "seznam", "sogou", "startpage", "swisscows", "quark", "qwant",
|
# "yandex", "privacywall", "mwmbl", "naver", "seznam", "sogou", "startpage", "swisscows", "quark", "qwant",
|
||||||
# "wikipedia" - set blank to turn it off by default.
|
# "wikipedia" - leave blank to turn it off by default.
|
||||||
autocomplete: "duckduckgo"
|
autocomplete: ""
|
||||||
# minimun characters to type before autocompleter starts
|
# minimun characters to type before autocompleter starts
|
||||||
autocomplete_min: 4
|
autocomplete_min: 4
|
||||||
# backend for the favicon near URL in search results.
|
# backend for the favicon near URL in search results.
|
||||||
# Available resolvers: "allesedv", "duckduckgo", "google", "kagi", "yandex" - leave blank to turn it off by default.
|
# Available resolvers: "allesedv", "duckduckgo", "google", "yandex" - leave blank to turn it off by default.
|
||||||
favicon_resolver: ""
|
favicon_resolver: ""
|
||||||
# Default search language - leave blank to detect from browser information or
|
# Default search language - leave blank to detect from browser information or
|
||||||
# use codes from 'languages.py'
|
# use codes from 'languages.py'
|
||||||
@@ -107,10 +107,11 @@ server:
|
|||||||
image_proxy: false
|
image_proxy: false
|
||||||
# 1.0 and 1.1 are supported
|
# 1.0 and 1.1 are supported
|
||||||
http_protocol_version: "1.0"
|
http_protocol_version: "1.0"
|
||||||
# POST keeps search queries out of the browsers history, but causes UX issues.
|
# POST queries are "more secure!" but are also the source of hard-to-locate
|
||||||
|
# annoyances, which is why GET may be better for end users and their browsers.
|
||||||
# see https://github.com/searxng/searxng/pull/3619
|
# see https://github.com/searxng/searxng/pull/3619
|
||||||
# Is overwritten by ${SEARXNG_METHOD}
|
# Is overwritten by ${SEARXNG_METHOD}
|
||||||
method: "GET"
|
method: "POST"
|
||||||
default_http_headers:
|
default_http_headers:
|
||||||
X-Content-Type-Options: nosniff
|
X-Content-Type-Options: nosniff
|
||||||
X-Download-Options: noopen
|
X-Download-Options: noopen
|
||||||
@@ -256,6 +257,9 @@ plugins:
|
|||||||
searx.plugins.tracker_url_remover.SXNGPlugin:
|
searx.plugins.tracker_url_remover.SXNGPlugin:
|
||||||
active: true
|
active: true
|
||||||
|
|
||||||
|
searx.plugins.ai_summary.SXNGPlugin:
|
||||||
|
active: false
|
||||||
|
|
||||||
|
|
||||||
# Configuration of the "Hostnames plugin":
|
# Configuration of the "Hostnames plugin":
|
||||||
#
|
#
|
||||||
@@ -283,6 +287,34 @@ plugins:
|
|||||||
# '(.*\.)?youtu\.be$': 'yt.example.com'
|
# '(.*\.)?youtu\.be$': 'yt.example.com'
|
||||||
#
|
#
|
||||||
|
|
||||||
|
# Configuration of the "AI Summary plugin", for more details see
|
||||||
|
# https://docs.searxng.org/admin/settings/settings_ai_summary.html
|
||||||
|
#
|
||||||
|
# ai_summary:
|
||||||
|
#
|
||||||
|
# # Base URL of an OpenAI compatible LLM server (Ollama, vLLM, LM Studio,
|
||||||
|
# # llama.cpp, Hugging Face TGI, ...), used as the default for the
|
||||||
|
# # ai_summary_server preference.
|
||||||
|
# base_url: "http://127.0.0.1:11434"
|
||||||
|
#
|
||||||
|
# # API key of the server above, if it requires authentication (e.g. vLLM or
|
||||||
|
# # llama.cpp with --api-key). Sent as "Authorization: Bearer" and only to
|
||||||
|
# # base_url, never to a server configured by a user in the preferences.
|
||||||
|
# api_key: ""
|
||||||
|
#
|
||||||
|
# # Default model; if empty, the first entry of models: is used.
|
||||||
|
# model: "llama3.2:3b"
|
||||||
|
#
|
||||||
|
# # Models suggested to the user in the preferences; if empty, the list
|
||||||
|
# # is requested from the LLM server (GET /v1/models) at startup.
|
||||||
|
# models:
|
||||||
|
# - "llama3.2:3b"
|
||||||
|
# - "gemma3:4b"
|
||||||
|
#
|
||||||
|
# # Ground summaries on the search results by default (users can still opt
|
||||||
|
# # in/out in their preferences).
|
||||||
|
# grounding: true
|
||||||
|
|
||||||
|
|
||||||
categories_as_tabs:
|
categories_as_tabs:
|
||||||
general:
|
general:
|
||||||
@@ -852,28 +884,28 @@ engines:
|
|||||||
shortcut: dog
|
shortcut: dog
|
||||||
dogpile_categ: search
|
dogpile_categ: search
|
||||||
categories: general
|
categories: general
|
||||||
inactive: true
|
disabled: true
|
||||||
|
|
||||||
- name: dogpile images
|
- name: dogpile images
|
||||||
engine: dogpile
|
engine: dogpile
|
||||||
shortcut: dogi
|
shortcut: dogi
|
||||||
dogpile_categ: images
|
dogpile_categ: images
|
||||||
categories: images
|
categories: images
|
||||||
inactive: true
|
disabled: true
|
||||||
|
|
||||||
- name: dogpile videos
|
- name: dogpile videos
|
||||||
engine: dogpile
|
engine: dogpile
|
||||||
shortcut: dogv
|
shortcut: dogv
|
||||||
dogpile_categ: videos
|
dogpile_categ: videos
|
||||||
categories: videos
|
categories: videos
|
||||||
inactive: true
|
disabled: true
|
||||||
|
|
||||||
- name: dogpile news
|
- name: dogpile news
|
||||||
engine: dogpile
|
engine: dogpile
|
||||||
shortcut: dogn
|
shortcut: dogn
|
||||||
dogpile_categ: news
|
dogpile_categ: news
|
||||||
categories: news
|
categories: news
|
||||||
inactive: true
|
disabled: true
|
||||||
|
|
||||||
# duckduckgo uses html.duckduckgo.com,
|
# duckduckgo uses html.duckduckgo.com,
|
||||||
# duckduckgo web uses duckduckgo.com
|
# duckduckgo web uses duckduckgo.com
|
||||||
@@ -1410,12 +1442,6 @@ engines:
|
|||||||
shortcut: iq
|
shortcut: iq
|
||||||
disabled: true
|
disabled: true
|
||||||
|
|
||||||
- name: jina
|
|
||||||
engine: jina
|
|
||||||
shortcut: ji
|
|
||||||
api_key: ""
|
|
||||||
inactive: true
|
|
||||||
|
|
||||||
# - name: kagi
|
# - name: kagi
|
||||||
# engine: kagi
|
# engine: kagi
|
||||||
# categories: [general, web]
|
# categories: [general, web]
|
||||||
@@ -1466,12 +1492,6 @@ engines:
|
|||||||
description: "Demo index of Kavunka, a statistical search engine"
|
description: "Demo index of Kavunka, a statistical search engine"
|
||||||
results: HTML
|
results: HTML
|
||||||
|
|
||||||
- name: keenable
|
|
||||||
engine: keenable
|
|
||||||
shortcut: keen
|
|
||||||
disabled: true
|
|
||||||
inactive: true
|
|
||||||
|
|
||||||
- name: kozmonavt
|
- name: kozmonavt
|
||||||
engine: xpath
|
engine: xpath
|
||||||
search_url: https://kozmonavt.su/s?q={query}
|
search_url: https://kozmonavt.su/s?q={query}
|
||||||
@@ -2027,6 +2047,41 @@ engines:
|
|||||||
shortcut: poc
|
shortcut: poc
|
||||||
disabled: true
|
disabled: true
|
||||||
|
|
||||||
|
- name: presearch
|
||||||
|
engine: presearch
|
||||||
|
search_type: search
|
||||||
|
categories: [general, web]
|
||||||
|
shortcut: ps
|
||||||
|
timeout: 4.0
|
||||||
|
disabled: true
|
||||||
|
|
||||||
|
- name: presearch images
|
||||||
|
engine: presearch
|
||||||
|
network: presearch
|
||||||
|
search_type: images
|
||||||
|
categories: [images, web]
|
||||||
|
timeout: 4.0
|
||||||
|
shortcut: psimg
|
||||||
|
disabled: true
|
||||||
|
|
||||||
|
- name: presearch videos
|
||||||
|
engine: presearch
|
||||||
|
network: presearch
|
||||||
|
search_type: videos
|
||||||
|
categories: [general, web]
|
||||||
|
timeout: 4.0
|
||||||
|
shortcut: psvid
|
||||||
|
disabled: true
|
||||||
|
|
||||||
|
- name: presearch news
|
||||||
|
engine: presearch
|
||||||
|
network: presearch
|
||||||
|
search_type: news
|
||||||
|
categories: [news, web]
|
||||||
|
timeout: 4.0
|
||||||
|
shortcut: psnews
|
||||||
|
disabled: true
|
||||||
|
|
||||||
- name: pub.dev
|
- name: pub.dev
|
||||||
engine: xpath
|
engine: xpath
|
||||||
shortcut: pd
|
shortcut: pd
|
||||||
@@ -2152,6 +2207,12 @@ engines:
|
|||||||
require_api_key: false
|
require_api_key: false
|
||||||
results: JSON
|
results: JSON
|
||||||
|
|
||||||
|
- name: reddit
|
||||||
|
engine: reddit
|
||||||
|
shortcut: re
|
||||||
|
page_size: 25
|
||||||
|
disabled: true
|
||||||
|
|
||||||
- name: reuters
|
- name: reuters
|
||||||
engine: reuters
|
engine: reuters
|
||||||
shortcut: reu
|
shortcut: reu
|
||||||
@@ -2584,16 +2645,6 @@ engines:
|
|||||||
shortcut: ydi
|
shortcut: ydi
|
||||||
disabled: true
|
disabled: true
|
||||||
|
|
||||||
- name: yandex api
|
|
||||||
engine: yandex_api
|
|
||||||
categories: [general, web]
|
|
||||||
shortcut: yda
|
|
||||||
inactive: true
|
|
||||||
# api_key: ""
|
|
||||||
# folder_id: ""
|
|
||||||
# search_type: SEARCH_TYPE_COM
|
|
||||||
# l10n: LOCALIZATION_EN
|
|
||||||
|
|
||||||
- name: yandex music
|
- name: yandex music
|
||||||
engine: yandex_music
|
engine: yandex_music
|
||||||
network: yandex
|
network: yandex
|
||||||
@@ -3276,14 +3327,6 @@ engines:
|
|||||||
# brave_category: goggles
|
# brave_category: goggles
|
||||||
# Goggles: # required! This should be a URL ending in .goggle
|
# Goggles: # required! This should be a URL ending in .goggle
|
||||||
|
|
||||||
- name: exaapi
|
|
||||||
engine: exaapi
|
|
||||||
shortcut: exa
|
|
||||||
api_key: ""
|
|
||||||
search_type: auto
|
|
||||||
content_mode: highlights
|
|
||||||
inactive: true
|
|
||||||
|
|
||||||
- name: lib.rs
|
- name: lib.rs
|
||||||
shortcut: lrs
|
shortcut: lrs
|
||||||
engine: lib_rs
|
engine: lib_rs
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from os.path import dirname, abspath
|
|||||||
import msgspec
|
import msgspec
|
||||||
|
|
||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
|
from .ai_summary import SettingsAISummary
|
||||||
from .brand import SettingsBrand
|
from .brand import SettingsBrand
|
||||||
from .sxng_locales import sxng_locales
|
from .sxng_locales import sxng_locales
|
||||||
from ._settings import SettingsPref
|
from ._settings import SettingsPref
|
||||||
@@ -192,7 +193,7 @@ SCHEMA: dict[str, t.Any] = {
|
|||||||
'brand': SettingsBrand,
|
'brand': SettingsBrand,
|
||||||
'search': {
|
'search': {
|
||||||
'safe_search': SettingsValue((0, 1, 2), 0),
|
'safe_search': SettingsValue((0, 1, 2), 0),
|
||||||
'autocomplete': SettingsValue(str, 'duckduckgo'),
|
'autocomplete': SettingsValue(str, ''),
|
||||||
'autocomplete_min': SettingsValue(int, 4),
|
'autocomplete_min': SettingsValue(int, 4),
|
||||||
'favicon_resolver': SettingsValue(str, ''),
|
'favicon_resolver': SettingsValue(str, ''),
|
||||||
'default_lang': SettingsValue(tuple(SXNG_LOCALE_TAGS + ['']), ''),
|
'default_lang': SettingsValue(tuple(SXNG_LOCALE_TAGS + ['']), ''),
|
||||||
@@ -219,7 +220,7 @@ SCHEMA: dict[str, t.Any] = {
|
|||||||
'base_url': SettingsValue((False, str), False, 'SEARXNG_BASE_URL'),
|
'base_url': SettingsValue((False, str), False, 'SEARXNG_BASE_URL'),
|
||||||
'image_proxy': SettingsValue(bool, False, 'SEARXNG_IMAGE_PROXY'),
|
'image_proxy': SettingsValue(bool, False, 'SEARXNG_IMAGE_PROXY'),
|
||||||
'http_protocol_version': SettingsValue(('1.0', '1.1'), '1.0'),
|
'http_protocol_version': SettingsValue(('1.0', '1.1'), '1.0'),
|
||||||
'method': SettingsValue(('POST', 'GET'), 'GET', 'SEARXNG_METHOD'),
|
'method': SettingsValue(('POST', 'GET'), 'POST', 'SEARXNG_METHOD'),
|
||||||
'default_http_headers': SettingsValue(dict, {}),
|
'default_http_headers': SettingsValue(dict, {}),
|
||||||
},
|
},
|
||||||
# redis is deprecated ..
|
# redis is deprecated ..
|
||||||
@@ -267,6 +268,7 @@ SCHEMA: dict[str, t.Any] = {
|
|||||||
'networks': {},
|
'networks': {},
|
||||||
},
|
},
|
||||||
'plugins': SettingsValue(dict, {}),
|
'plugins': SettingsValue(dict, {}),
|
||||||
|
'ai_summary': SettingsAISummary,
|
||||||
'categories_as_tabs': SettingsValue(dict, CATEGORIES_AS_TABS),
|
'categories_as_tabs': SettingsValue(dict, CATEGORIES_AS_TABS),
|
||||||
'engines': SettingsValue(list, []),
|
'engines': SettingsValue(list, []),
|
||||||
'doi_resolvers': {},
|
'doi_resolvers': {},
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -1,2 +0,0 @@
|
|||||||
var e=class{id;constructor(e){this.id=e,queueMicrotask(()=>this.invoke())}async invoke(){try{console.debug(`[PLUGIN] ${this.id}: Running...`);let e=await this.run();if(!e)return;console.debug(`[PLUGIN] ${this.id}: Running post-exec...`),await this.post(e)}catch(e){console.error(`[PLUGIN] ${this.id}:`,e)}finally{console.debug(`[PLUGIN] ${this.id}: Done.`)}}};export{e as t};
|
|
||||||
//# sourceMappingURL=BuurKv-k.min.js.map
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"BuurKv-k.min.js","names":[],"sources":["../../../../../client/simple/src/js/Plugin.ts"],"sourcesContent":["// SPDX-License-Identifier: AGPL-3.0-or-later\n\n/**\n * Base class for client-side plugins.\n *\n * @remarks\n * Handle conditional loading of the plugin in:\n *\n * - client/simple/src/js/router.ts\n *\n * @abstract\n */\nexport abstract class Plugin {\n /**\n * Plugin name.\n */\n protected readonly id: string;\n\n /**\n * @remarks\n * Don't hold references of this instance outside the class.\n */\n protected constructor(id: string) {\n this.id = id;\n\n queueMicrotask(() => this.invoke());\n }\n\n private async invoke(): Promise<void> {\n try {\n console.debug(`[PLUGIN] ${this.id}: Running...`);\n const result = await this.run();\n if (!result) return;\n\n console.debug(`[PLUGIN] ${this.id}: Running post-exec...`);\n // @ts-expect-error\n void (await this.post(result as NonNullable<Awaited<ReturnType<this[\"run\"]>>>));\n } catch (error) {\n console.error(`[PLUGIN] ${this.id}:`, error);\n } finally {\n console.debug(`[PLUGIN] ${this.id}: Done.`);\n }\n }\n\n /**\n * Plugin goes here.\n *\n * @remarks\n * The plugin is already loaded at this point. If you wish to execute\n * conditions to exit early, consider moving the logic to:\n *\n * - client/simple/src/js/router.ts\n *\n * ...to avoid unnecessarily loading this plugin on the client.\n */\n protected abstract run(): Promise<unknown>;\n\n /**\n * Post-execution hook.\n *\n * @remarks\n * The hook is only executed if `#run()` returns a truthy value.\n */\n // @ts-expect-error\n protected abstract post(result: NonNullable<Awaited<ReturnType<this[\"run\"]>>>): Promise<void>;\n}\n"],"mappings":"AAYA,IAAsB,EAAtB,KAA6B,CAI3B,GAMA,YAAsB,EAAY,CAChC,KAAK,GAAK,EAEV,mBAAqB,KAAK,OAAO,CAAC,CACpC,CAEA,MAAc,QAAwB,CACpC,GAAI,CACF,QAAQ,MAAM,YAAY,KAAK,GAAG,aAAa,EAC/C,IAAM,EAAS,MAAM,KAAK,IAAI,EAC9B,GAAI,CAAC,EAAQ,OAEb,QAAQ,MAAM,YAAY,KAAK,GAAG,uBAAuB,EAEzD,MAAY,KAAK,KAAK,CAAuD,CAC/E,OAAS,EAAO,CACd,QAAQ,MAAM,YAAY,KAAK,GAAG,GAAI,CAAK,CAC7C,QAAU,CACR,QAAQ,MAAM,YAAY,KAAK,GAAG,QAAQ,CAC5C,CACF,CAuBF"}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
import{i as e,n as t,t as n}from"../sxng-core.min.js";import{t as r}from"./DK4yUVpy.min.js";var i=async(i,a)=>{try{let o=await(await n(`GET`,`./autocompleter?q=${a}`)).json(),s=document.querySelector(`.autocomplete`);r(s);let c=document.querySelector(`.autocomplete ul`);if(r(c),s.classList.add(`open`),c.replaceChildren(),o?.[1]?.length===0){let t=Object.assign(document.createElement(`li`),{className:`no-item-found`,textContent:e.translations?.no_item_found??`No results found`});c.append(t);return}let l=new DocumentFragment;for(let e of o[1]){let n=Object.assign(document.createElement(`li`),{textContent:e});t(`mousedown`,n,()=>{i.value=e,document.querySelector(`#search`)?.submit()}),l.append(n)}c.append(l)}catch(e){console.error(`Error fetching autocomplete results:`,e)}},a=document.getElementById(`q`);r(a);var o;t(`input`,a,()=>{clearTimeout(o);let t=a.value,n=e.autocomplete_min??2;t.length<n||(o=window.setTimeout(async()=>{t===a.value&&await i(a,t)},300))});var s=document.querySelector(`.autocomplete`),c=document.querySelector(`.autocomplete ul`);c&&(t(`keydown`,a,e=>{e.key===`Escape`&&s?.classList.remove(`open`)}),t(`keyup`,a,e=>{let t=[...c.children],n=t.findIndex(e=>e.classList.contains(`active`)),r=-1;switch(e.key){case`ArrowUp`:{let e=t[n];e&&n>=0&&e.classList.remove(`active`),r=(n-1+t.length)%t.length;break}case`ArrowDown`:{let e=t[n];e&&n>=0&&e.classList.remove(`active`),r=(n+1)%t.length;break}case`Enter`:s&&s.classList.remove(`open`);break;default:break}if(r!==-1){let e=t[r];if(e&&(e.classList.add(`active`),!e.classList.contains(`no-item-found`))){let t=document.getElementById(`q`);t&&(t.value=e.textContent??``)}}}),t(`blur`,a,()=>{s?.classList.remove(`open`)}),t(`focus`,a,()=>{s?.classList.add(`open`)}));
|
|
||||||
//# sourceMappingURL=C21EfLAC.min.js.map
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -1,2 +0,0 @@
|
|||||||
import{t as e}from"./BuurKv-k.min.js";import{i as t,t as n}from"../sxng-core.min.js";import{t as r}from"./DK4yUVpy.min.js";import{t as i}from"./DcK-mo-Y.min.js";var a=class extends e{constructor(){super(`infiniteScroll`)}async run(){let e=i(`results`).classList.contains(`only_template_images`),a=`article.result:last-child`,o=document.createElement(`div`);o.className=`loader`;let s=async i=>{let a=document.querySelector(`#search`);r(a);let s=document.querySelector(`#pagination form.next_page`);r(s);let c=a.getAttribute(`action`);if(!c)throw Error(`Form action not defined`);let l=document.querySelector(`#pagination`);r(l),l.replaceChildren(o);try{let t=await(await n(`POST`,c,{body:new FormData(s)})).text();if(!t)return;let r=new DOMParser().parseFromString(t,`text/html`),a=r.querySelectorAll(`#urls article`),o=r.querySelector(`#pagination`);document.querySelector(`#pagination`)?.remove();let l=document.querySelector(`#urls`);if(!l)throw Error(`URLs element not found`);a.length>0&&!e&&l.appendChild(document.createElement(`hr`)),l.append(...a),o&&(document.querySelector(`#results`)?.appendChild(o),i())}catch(e){console.error(`Error loading next page:`,e);let n=Object.assign(document.createElement(`div`),{textContent:t.translations?.error_loading_next_page??`Error loading next page`,className:`dialog-error`});n.setAttribute(`role`,`alert`),document.querySelector(`#pagination`)?.replaceChildren(n)}},c=new IntersectionObserver(async e=>{let[t]=e;t?.isIntersecting&&(c.unobserve(t.target),await s(()=>{let e=document.querySelector(a);e&&c.observe(e)}))},{rootMargin:`320px`}),l=document.querySelector(a);l&&c.observe(l)}async post(){}};export{a as default};
|
|
||||||
//# sourceMappingURL=D3mcqWOe.min.js.map
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import{a as e,i as t}from"../sxng-core.min.js";import{t as n}from"./DK4yUVpy.min.js";var r=5,i=12,a=300,o=1e3,s=class s extends e{messages=[];context=[];controller;constructor(){super(`ai_summary`)}async run(){let e=document.getElementById(`ai_summary`);if(e)try{let{query:t}=e.dataset;if(!t)return;e.dataset.grounding===`1`&&(this.context=s.collectContext()),this.wireControls(e),this.messages.push({role:`user`,content:t}),await this.exchange(e)}catch(t){s.showError(e,t)}}async post(){}static collectContext(){let e=[];for(let t of document.querySelectorAll(`#urls article.result`)){if(e.length>=r)break;let n=t.querySelector(`h3 a`);n&&e.push({title:(n.textContent??``).trim().slice(0,a),url:n.href,snippet:(t.querySelector(`.content`)?.textContent??``).trim().slice(0,o)})}return e}wireControls(e){let t=e.querySelector(`.ai-summary-body`),r=e.querySelector(`.ai-summary-more`),a=e.querySelector(`.ai-summary-followup`);n(t),n(r),n(a),r.addEventListener(`click`,()=>{let e=t.classList.toggle(`collapsed`);r.textContent=e?r.dataset.btnTextCollapsed??``:r.dataset.btnTextNotCollapsed??``,a.classList.toggle(`invisible`,e)}),a.addEventListener(`submit`,t=>{t.preventDefault();let r=a.querySelector(`input`);n(r);let o=r.value.trim();if(!o||this.controller)return;r.value=``,this.messages.push({role:`user`,content:o}),this.messages.splice(0,this.messages.length-(i-1));let s=Object.assign(document.createElement(`p`),{textContent:o,className:`ai-summary-question`});e.querySelector(`.ai-summary-answers`)?.append(s),this.exchange(e)})}async exchange(e){let t=e.querySelector(`.ai-summary-answers`);n(t);let r=Object.assign(document.createElement(`p`),{className:`ai-summary-content typing`});t.append(r);let i=new AbortController;this.controller=i;let a=``,o=t=>{if(!t.trim())return;let n=JSON.parse(t);if(n.error)throw Error(n.error);if(n.delta&&(a+=n.delta,r.textContent=a,this.updateMoreButton(e)),n.done&&n.model){let t=e.querySelector(`.ai-summary-model`);t&&(t.textContent=n.model)}};try{let e=await fetch(`./ai_summary`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({messages:this.messages,context:this.context}),signal:i.signal});if(!e.ok)throw Error(`HTTP ${e.status}`);if(e.body){let t=e.body.getReader(),n=new TextDecoder,r=``;for(;;){let{done:e,value:i}=await t.read();if(e)break;r+=n.decode(i,{stream:!0});let a=r.split(`
|
||||||
|
`);r=a.pop()??``;for(let e of a)o(e)}o(r)}else{let t=await e.text();for(let e of t.split(`
|
||||||
|
`))o(e)}this.messages.push({role:`assistant`,content:a})}catch(t){s.showError(e,t)}finally{r.classList.remove(`typing`),this.controller=void 0,this.updateMoreButton(e)}}static showError(e,n){console.error(`Error loading AI summary:`,n);let r=t.translations?.error_loading_ai_summary??`Error loading the AI summary`,i=n instanceof Error&&n.message?` (${n.message})`:``,a=Object.assign(document.createElement(`div`),{textContent:`${r}${i}`,className:`dialog-error`});a.setAttribute(`role`,`alert`),(e.querySelector(`.ai-summary-answers`)??e).append(a)}updateMoreButton(e){let t=e.querySelector(`.ai-summary-body`),n=e.querySelector(`.ai-summary-more`);t&&n&&(!t.classList.contains(`collapsed`)||t.scrollHeight>t.clientHeight+4)&&n.classList.remove(`invisible`)}};export{s as default};
|
||||||
|
//# sourceMappingURL=DBBLNmaq.min.js.map
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
|
|||||||
|
import{a as e,i as t,t as n}from"../sxng-core.min.js";import{t as r}from"./DK4yUVpy.min.js";import{t as i}from"./DcK-mo-Y.min.js";var a=class extends e{constructor(){super(`infiniteScroll`)}async run(){let e=i(`results`).classList.contains(`only_template_images`),a=`article.result:last-child`,o=document.createElement(`div`);o.className=`loader`;let s=async i=>{let a=document.querySelector(`#search`);r(a);let s=document.querySelector(`#pagination form.next_page`);r(s);let c=a.getAttribute(`action`);if(!c)throw Error(`Form action not defined`);let l=document.querySelector(`#pagination`);r(l),l.replaceChildren(o);try{let t=await(await n(`POST`,c,{body:new FormData(s)})).text();if(!t)return;let r=new DOMParser().parseFromString(t,`text/html`),a=r.querySelectorAll(`#urls article`),o=r.querySelector(`#pagination`);document.querySelector(`#pagination`)?.remove();let l=document.querySelector(`#urls`);if(!l)throw Error(`URLs element not found`);a.length>0&&!e&&l.appendChild(document.createElement(`hr`)),l.append(...a),o&&(document.querySelector(`#results`)?.appendChild(o),i())}catch(e){console.error(`Error loading next page:`,e);let n=Object.assign(document.createElement(`div`),{textContent:t.translations?.error_loading_next_page??`Error loading next page`,className:`dialog-error`});n.setAttribute(`role`,`alert`),document.querySelector(`#pagination`)?.replaceChildren(n)}},c=new IntersectionObserver(async e=>{let[t]=e;t?.isIntersecting&&(c.unobserve(t.target),await s(()=>{let e=document.querySelector(a);e&&c.observe(e)}))},{rootMargin:`320px`}),l=document.querySelector(a);l&&c.observe(l)}async post(){}};export{a as default};
|
||||||
|
//# sourceMappingURL=DpvWr1cn.min.js.map
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
|
|||||||
|
import{i as e,n as t,t as n}from"../sxng-core.min.js";import{t as r}from"./DK4yUVpy.min.js";var i=async(i,a)=>{try{let o;o=e.method===`GET`?await n(`GET`,`./autocompleter?q=${a}`):await n(`POST`,`./autocompleter`,{body:new URLSearchParams({q:a})});let s=await o.json(),c=document.querySelector(`.autocomplete`);r(c);let l=document.querySelector(`.autocomplete ul`);if(r(l),c.classList.add(`open`),l.replaceChildren(),s?.[1]?.length===0){let t=Object.assign(document.createElement(`li`),{className:`no-item-found`,textContent:e.translations?.no_item_found??`No results found`});l.append(t);return}let u=new DocumentFragment;for(let e of s[1]){let n=Object.assign(document.createElement(`li`),{textContent:e});t(`mousedown`,n,()=>{i.value=e,document.querySelector(`#search`)?.submit()}),u.append(n)}l.append(u)}catch(e){console.error(`Error fetching autocomplete results:`,e)}},a=document.getElementById(`q`);r(a);var o;t(`input`,a,()=>{clearTimeout(o);let t=a.value,n=e.autocomplete_min??2;t.length<n||(o=window.setTimeout(async()=>{t===a.value&&await i(a,t)},300))});var s=document.querySelector(`.autocomplete`),c=document.querySelector(`.autocomplete ul`);c&&(t(`keydown`,a,e=>{e.key===`Escape`&&s?.classList.remove(`open`)}),t(`keyup`,a,e=>{let t=[...c.children],n=t.findIndex(e=>e.classList.contains(`active`)),r=-1;switch(e.key){case`ArrowUp`:{let e=t[n];e&&n>=0&&e.classList.remove(`active`),r=(n-1+t.length)%t.length;break}case`ArrowDown`:{let e=t[n];e&&n>=0&&e.classList.remove(`active`),r=(n+1)%t.length;break}case`Enter`:s&&s.classList.remove(`open`);break;default:break}if(r!==-1){let e=t[r];if(e&&(e.classList.add(`active`),!e.classList.contains(`no-item-found`))){let t=document.getElementById(`q`);t&&(t.value=e.textContent??``)}}}),t(`blur`,a,()=>{s?.classList.remove(`open`)}),t(`focus`,a,()=>{s?.classList.add(`open`)}));
|
||||||
|
//# sourceMappingURL=DvCYLbJr.min.js.map
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,8 +1,4 @@
|
|||||||
{
|
{
|
||||||
"_BuurKv-k.min.js": {
|
|
||||||
"file": "chunk/BuurKv-k.min.js",
|
|
||||||
"name": "plugin"
|
|
||||||
},
|
|
||||||
"_DK4yUVpy.min.js": {
|
"_DK4yUVpy.min.js": {
|
||||||
"file": "chunk/DK4yUVpy.min.js",
|
"file": "chunk/DK4yUVpy.min.js",
|
||||||
"name": "assertelement"
|
"name": "assertelement"
|
||||||
@@ -23,6 +19,7 @@
|
|||||||
"src/js/plugin/MapView.ts",
|
"src/js/plugin/MapView.ts",
|
||||||
"src/js/plugin/InfiniteScroll.ts",
|
"src/js/plugin/InfiniteScroll.ts",
|
||||||
"src/js/plugin/Calculator.ts",
|
"src/js/plugin/Calculator.ts",
|
||||||
|
"src/js/plugin/AiSummary.ts",
|
||||||
"src/js/main/keyboard.ts",
|
"src/js/main/keyboard.ts",
|
||||||
"src/js/main/search.ts",
|
"src/js/main/search.ts",
|
||||||
"src/js/main/autocomplete.ts",
|
"src/js/main/autocomplete.ts",
|
||||||
@@ -31,7 +28,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/js/main/autocomplete.ts": {
|
"src/js/main/autocomplete.ts": {
|
||||||
"file": "chunk/C21EfLAC.min.js",
|
"file": "chunk/DvCYLbJr.min.js",
|
||||||
"name": "autocomplete",
|
"name": "autocomplete",
|
||||||
"src": "src/js/main/autocomplete.ts",
|
"src": "src/js/main/autocomplete.ts",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
@@ -80,35 +77,44 @@
|
|||||||
"_DcK-mo-Y.min.js"
|
"_DcK-mo-Y.min.js"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"src/js/plugin/AiSummary.ts": {
|
||||||
|
"file": "chunk/DBBLNmaq.min.js",
|
||||||
|
"name": "aisummary",
|
||||||
|
"src": "src/js/plugin/AiSummary.ts",
|
||||||
|
"isDynamicEntry": true,
|
||||||
|
"imports": [
|
||||||
|
"src/js/index.ts",
|
||||||
|
"_DK4yUVpy.min.js"
|
||||||
|
]
|
||||||
|
},
|
||||||
"src/js/plugin/Calculator.ts": {
|
"src/js/plugin/Calculator.ts": {
|
||||||
"file": "chunk/BfLIj3Of.min.js",
|
"file": "chunk/C8c7HJzp.min.js",
|
||||||
"name": "calculator",
|
"name": "calculator",
|
||||||
"src": "src/js/plugin/Calculator.ts",
|
"src": "src/js/plugin/Calculator.ts",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"_BuurKv-k.min.js",
|
"src/js/index.ts",
|
||||||
"_DcK-mo-Y.min.js"
|
"_DcK-mo-Y.min.js"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/js/plugin/InfiniteScroll.ts": {
|
"src/js/plugin/InfiniteScroll.ts": {
|
||||||
"file": "chunk/D3mcqWOe.min.js",
|
"file": "chunk/DpvWr1cn.min.js",
|
||||||
"name": "infinitescroll",
|
"name": "infinitescroll",
|
||||||
"src": "src/js/plugin/InfiniteScroll.ts",
|
"src": "src/js/plugin/InfiniteScroll.ts",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"_BuurKv-k.min.js",
|
|
||||||
"src/js/index.ts",
|
"src/js/index.ts",
|
||||||
"_DK4yUVpy.min.js",
|
"_DK4yUVpy.min.js",
|
||||||
"_DcK-mo-Y.min.js"
|
"_DcK-mo-Y.min.js"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"src/js/plugin/MapView.ts": {
|
"src/js/plugin/MapView.ts": {
|
||||||
"file": "chunk/Bc8fcwWx.min.js",
|
"file": "chunk/BnnvKC7b.min.js",
|
||||||
"name": "mapview",
|
"name": "mapview",
|
||||||
"src": "src/js/plugin/MapView.ts",
|
"src": "src/js/plugin/MapView.ts",
|
||||||
"isDynamicEntry": true,
|
"isDynamicEntry": true,
|
||||||
"imports": [
|
"imports": [
|
||||||
"_BuurKv-k.min.js"
|
"src/js/index.ts"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"sxng-mapview.min.css"
|
"sxng-mapview.min.css"
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./chunk/Bc8fcwWx.min.js","./chunk/BuurKv-k.min.js","./sxng-mapview.min.css","./chunk/D3mcqWOe.min.js","./chunk/DK4yUVpy.min.js","./chunk/DcK-mo-Y.min.js","./chunk/BfLIj3Of.min.js","./chunk/C93hSkpT.min.js","./chunk/5Ako-qGW.min.js","./chunk/C21EfLAC.min.js","./chunk/od7pNHfk.min.js","./chunk/e2-9fzwE.min.js"])))=>i.map(i=>d[i]);
|
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./chunk/BnnvKC7b.min.js","./sxng-mapview.min.css","./chunk/DpvWr1cn.min.js","./chunk/DK4yUVpy.min.js","./chunk/DcK-mo-Y.min.js","./chunk/C8c7HJzp.min.js","./chunk/DBBLNmaq.min.js","./chunk/C93hSkpT.min.js","./chunk/5Ako-qGW.min.js","./chunk/DvCYLbJr.min.js","./chunk/od7pNHfk.min.js","./chunk/e2-9fzwE.min.js"])))=>i.map(i=>d[i]);
|
||||||
var e={index:`index`,results:`results`,preferences:`preferences`,unknown:`unknown`},t={closeDetail:void 0,scrollPageToSelected:void 0,selectImage:void 0,selectNext:void 0,selectPrevious:void 0},n=()=>{let t=document.querySelector(`meta[name="endpoint"]`)?.getAttribute(`content`);return t&&t in e?t:e.unknown},r=()=>{let e=document.querySelector(`script[client_settings]`)?.getAttribute(`client_settings`);if(!e)return{};try{return JSON.parse(atob(e))}catch(e){return console.error(`Failed to load client_settings:`,e),{}}},i=async(e,t,n)=>{let r=new AbortController,i=setTimeout(()=>r.abort(),n?.timeout??3e4),a=await fetch(t,{body:n?.body,method:e,signal:r.signal}).finally(()=>clearTimeout(i));if(!a.ok)throw Error(a.statusText);return a},a=(e,t,n,r)=>{if(typeof t!=`string`){t.addEventListener(e,n,r);return}document.addEventListener(e,e=>{for(let r of e.composedPath())if(r instanceof HTMLElement&&r.matches(t)){try{n.call(r,e)}catch(e){console.error(e)}break}},r)},o=(e,t)=>{for(let e of t?.on??[])if(!e)return;document.readyState===`loading`?a(`DOMContentLoaded`,document,e,{once:!0}):e()},s=n(),c=r(),l=(e,t)=>{u(t)&&e()},u=e=>{switch(e.on){case`global`:return!0;case`endpoint`:return!!e.where.includes(s)}},d=`modulepreload`,f=function(e,t){return new URL(e,t).href},p={},m=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,import.meta.url).href}r=o(t.map(t=>{if(t=f(t,n),t=s(t),t in p)return;p[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:d,r||(i.as=`script`),i.crossOrigin=``,i.href=t,a&&i.setAttribute(`nonce`,a),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})};o(()=>{document.documentElement.classList.remove(`no-js`),document.documentElement.classList.add(`js`),a(`click`,`.close`,function(){this.parentNode?.classList.add(`invisible`)}),a(`click`,`.searxng_init_map`,async function(t){t.preventDefault(),this.classList.remove(`searxng_init_map`),l(()=>m(async()=>{let{default:e}=await import(`./chunk/Bc8fcwWx.min.js`);return{default:e}},__vite__mapDeps([0,1,2]),import.meta.url).then(({default:e})=>new e(this)),{on:`endpoint`,where:[e.results]})}),c.plugins?.includes(`infiniteScroll`)&&l(()=>m(async()=>{let{default:e}=await import(`./chunk/D3mcqWOe.min.js`);return{default:e}},__vite__mapDeps([3,1,4,5]),import.meta.url).then(({default:e})=>new e),{on:`endpoint`,where:[e.results]}),c.plugins?.includes(`calculator`)&&l(()=>m(async()=>{let{default:e}=await import(`./chunk/BfLIj3Of.min.js`);return{default:e}},__vite__mapDeps([6,1,5,4]),import.meta.url).then(({default:e})=>new e),{on:`endpoint`,where:[e.results]})}),o(()=>{m(()=>import(`./chunk/C93hSkpT.min.js`),__vite__mapDeps([7,4]),import.meta.url),m(()=>import(`./chunk/5Ako-qGW.min.js`),__vite__mapDeps([8,5,4]),import.meta.url),c.autocomplete&&m(()=>import(`./chunk/C21EfLAC.min.js`),__vite__mapDeps([9,4]),import.meta.url)},{on:[s===e.index]}),o(()=>{m(()=>import(`./chunk/C93hSkpT.min.js`),__vite__mapDeps([7,4]),import.meta.url),m(()=>import(`./chunk/od7pNHfk.min.js`),__vite__mapDeps([10,4]),import.meta.url),m(()=>import(`./chunk/5Ako-qGW.min.js`),__vite__mapDeps([8,5,4]),import.meta.url),c.autocomplete&&m(()=>import(`./chunk/C21EfLAC.min.js`),__vite__mapDeps([9,4]),import.meta.url)},{on:[s===e.results]}),o(()=>{m(()=>import(`./chunk/e2-9fzwE.min.js`),__vite__mapDeps([11,4]),import.meta.url)},{on:[s===e.preferences]});export{c as i,a as n,t as r,i as t};
|
var e=class{id;constructor(e){this.id=e,queueMicrotask(()=>this.invoke())}async invoke(){try{console.debug(`[PLUGIN] ${this.id}: Running...`);let e=await this.run();if(!e)return;console.debug(`[PLUGIN] ${this.id}: Running post-exec...`),await this.post(e)}catch(e){console.error(`[PLUGIN] ${this.id}:`,e)}finally{console.debug(`[PLUGIN] ${this.id}: Done.`)}}},t={index:`index`,results:`results`,preferences:`preferences`,unknown:`unknown`},n={closeDetail:void 0,scrollPageToSelected:void 0,selectImage:void 0,selectNext:void 0,selectPrevious:void 0},r=()=>{let e=document.querySelector(`meta[name="endpoint"]`)?.getAttribute(`content`);return e&&e in t?e:t.unknown},i=()=>{let e=document.querySelector(`script[client_settings]`)?.getAttribute(`client_settings`);if(!e)return{};try{return JSON.parse(atob(e))}catch(e){return console.error(`Failed to load client_settings:`,e),{}}},a=async(e,t,n)=>{let r=new AbortController,i=setTimeout(()=>r.abort(),n?.timeout??3e4),a=await fetch(t,{body:n?.body,method:e,signal:r.signal}).finally(()=>clearTimeout(i));if(!a.ok)throw Error(a.statusText);return a},o=(e,t,n,r)=>{if(typeof t!=`string`){t.addEventListener(e,n,r);return}document.addEventListener(e,e=>{for(let r of e.composedPath())if(r instanceof HTMLElement&&r.matches(t)){try{n.call(r,e)}catch(e){console.error(e)}break}},r)},s=(e,t)=>{for(let e of t?.on??[])if(!e)return;document.readyState===`loading`?o(`DOMContentLoaded`,document,e,{once:!0}):e()},c=r(),l=i(),u=(e,t)=>{d(t)&&e()},d=e=>{switch(e.on){case`global`:return!0;case`endpoint`:return!!e.where.includes(c)}},f=`modulepreload`,p=function(e,t){return new URL(e,t).href},m={},h=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,import.meta.url).href}r=o(t.map(t=>{if(t=p(t,n),t=s(t),t in m)return;m[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:f,r||(i.as=`script`),i.crossOrigin=``,i.href=t,a&&i.setAttribute(`nonce`,a),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})};s(()=>{document.documentElement.classList.remove(`no-js`),document.documentElement.classList.add(`js`),o(`click`,`.close`,function(){this.parentNode?.classList.add(`invisible`)}),o(`click`,`.searxng_init_map`,async function(e){e.preventDefault(),this.classList.remove(`searxng_init_map`),u(()=>h(async()=>{let{default:e}=await import(`./chunk/BnnvKC7b.min.js`);return{default:e}},__vite__mapDeps([0,1]),import.meta.url).then(({default:e})=>new e(this)),{on:`endpoint`,where:[t.results]})}),l.plugins?.includes(`infiniteScroll`)&&u(()=>h(async()=>{let{default:e}=await import(`./chunk/DpvWr1cn.min.js`);return{default:e}},__vite__mapDeps([2,3,4]),import.meta.url).then(({default:e})=>new e),{on:`endpoint`,where:[t.results]}),l.plugins?.includes(`calculator`)&&u(()=>h(async()=>{let{default:e}=await import(`./chunk/C8c7HJzp.min.js`);return{default:e}},__vite__mapDeps([5,4,3]),import.meta.url).then(({default:e})=>new e),{on:`endpoint`,where:[t.results]}),l.plugins?.includes(`ai_summary`)&&u(()=>h(async()=>{let{default:e}=await import(`./chunk/DBBLNmaq.min.js`);return{default:e}},__vite__mapDeps([6,3]),import.meta.url).then(({default:e})=>new e),{on:`endpoint`,where:[t.results]})}),s(()=>{h(()=>import(`./chunk/C93hSkpT.min.js`),__vite__mapDeps([7,3]),import.meta.url),h(()=>import(`./chunk/5Ako-qGW.min.js`),__vite__mapDeps([8,4,3]),import.meta.url),l.autocomplete&&h(()=>import(`./chunk/DvCYLbJr.min.js`),__vite__mapDeps([9,3]),import.meta.url)},{on:[c===t.index]}),s(()=>{h(()=>import(`./chunk/C93hSkpT.min.js`),__vite__mapDeps([7,3]),import.meta.url),h(()=>import(`./chunk/od7pNHfk.min.js`),__vite__mapDeps([10,3]),import.meta.url),h(()=>import(`./chunk/5Ako-qGW.min.js`),__vite__mapDeps([8,4,3]),import.meta.url),l.autocomplete&&h(()=>import(`./chunk/DvCYLbJr.min.js`),__vite__mapDeps([9,3]),import.meta.url)},{on:[c===t.results]}),s(()=>{h(()=>import(`./chunk/e2-9fzwE.min.js`),__vite__mapDeps([11,3]),import.meta.url)},{on:[c===t.preferences]});export{e as a,l as i,o as n,n as r,a as t};
|
||||||
//# sourceMappingURL=sxng-core.min.js.map
|
//# sourceMappingURL=sxng-core.min.js.map
|
||||||
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -15,10 +15,8 @@ sxng_locales = (
|
|||||||
('bg', 'Български', '', 'Bulgarian', '\U0001f310'),
|
('bg', 'Български', '', 'Bulgarian', '\U0001f310'),
|
||||||
('bg-BG', 'Български', 'България', 'Bulgarian', '\U0001f1e7\U0001f1ec'),
|
('bg-BG', 'Български', 'България', 'Bulgarian', '\U0001f1e7\U0001f1ec'),
|
||||||
('ca', 'Català', '', 'Catalan', '\U0001f310'),
|
('ca', 'Català', '', 'Catalan', '\U0001f310'),
|
||||||
('ca-ES', 'Català', 'Espanya', 'Catalan', '\U0001f1ea\U0001f1f8'),
|
|
||||||
('cs', 'Čeština', '', 'Czech', '\U0001f310'),
|
('cs', 'Čeština', '', 'Czech', '\U0001f310'),
|
||||||
('cs-CZ', 'Čeština', 'Česko', 'Czech', '\U0001f1e8\U0001f1ff'),
|
('cs-CZ', 'Čeština', 'Česko', 'Czech', '\U0001f1e8\U0001f1ff'),
|
||||||
('cy', 'Cymraeg', '', 'Welsh', '\U0001f310'),
|
|
||||||
('da', 'Dansk', '', 'Danish', '\U0001f310'),
|
('da', 'Dansk', '', 'Danish', '\U0001f310'),
|
||||||
('da-DK', 'Dansk', 'Danmark', 'Danish', '\U0001f1e9\U0001f1f0'),
|
('da-DK', 'Dansk', 'Danmark', 'Danish', '\U0001f1e9\U0001f1f0'),
|
||||||
('de', 'Deutsch', '', 'German', '\U0001f310'),
|
('de', 'Deutsch', '', 'German', '\U0001f310'),
|
||||||
@@ -51,7 +49,6 @@ sxng_locales = (
|
|||||||
('es-VE', 'Español', 'Venezuela', 'Spanish', '\U0001f1fb\U0001f1ea'),
|
('es-VE', 'Español', 'Venezuela', 'Spanish', '\U0001f1fb\U0001f1ea'),
|
||||||
('et', 'Eesti', '', 'Estonian', '\U0001f310'),
|
('et', 'Eesti', '', 'Estonian', '\U0001f310'),
|
||||||
('et-EE', 'Eesti', 'Eesti', 'Estonian', '\U0001f1ea\U0001f1ea'),
|
('et-EE', 'Eesti', 'Eesti', 'Estonian', '\U0001f1ea\U0001f1ea'),
|
||||||
('fa', 'فارسی', '', 'Persian', '\U0001f310'),
|
|
||||||
('fi', 'Suomi', '', 'Finnish', '\U0001f310'),
|
('fi', 'Suomi', '', 'Finnish', '\U0001f310'),
|
||||||
('fi-FI', 'Suomi', 'Suomi', 'Finnish', '\U0001f1eb\U0001f1ee'),
|
('fi-FI', 'Suomi', 'Suomi', 'Finnish', '\U0001f1eb\U0001f1ee'),
|
||||||
('fil', 'Filipino', '', 'Filipino', '\U0001f310'),
|
('fil', 'Filipino', '', 'Filipino', '\U0001f310'),
|
||||||
@@ -77,10 +74,6 @@ sxng_locales = (
|
|||||||
('ja-JP', '日本語', '日本', 'Japanese', '\U0001f1ef\U0001f1f5'),
|
('ja-JP', '日本語', '日本', 'Japanese', '\U0001f1ef\U0001f1f5'),
|
||||||
('ko', '한국어', '', 'Korean', '\U0001f310'),
|
('ko', '한국어', '', 'Korean', '\U0001f310'),
|
||||||
('ko-KR', '한국어', '대한민국', 'Korean', '\U0001f1f0\U0001f1f7'),
|
('ko-KR', '한국어', '대한민국', 'Korean', '\U0001f1f0\U0001f1f7'),
|
||||||
('lt', 'Lietuvių', '', 'Lithuanian', '\U0001f310'),
|
|
||||||
('lt-LT', 'Lietuvių', 'Lietuva', 'Lithuanian', '\U0001f1f1\U0001f1f9'),
|
|
||||||
('lv', 'Latviešu', '', 'Latvian', '\U0001f310'),
|
|
||||||
('lv-LV', 'Latviešu', 'Latvija', 'Latvian', '\U0001f1f1\U0001f1fb'),
|
|
||||||
('mi', 'Māori', '', 'Māori', '\U0001f310'),
|
('mi', 'Māori', '', 'Māori', '\U0001f310'),
|
||||||
('mi-NZ', 'Māori', 'Aotearoa', 'Māori', '\U0001f1f3\U0001f1ff'),
|
('mi-NZ', 'Māori', 'Aotearoa', 'Māori', '\U0001f1f3\U0001f1ff'),
|
||||||
('nb', 'Norsk Bokmål', '', 'Norwegian Bokmål', '\U0001f310'),
|
('nb', 'Norsk Bokmål', '', 'Norwegian Bokmål', '\U0001f310'),
|
||||||
@@ -100,9 +93,6 @@ sxng_locales = (
|
|||||||
('ru', 'Русский', '', 'Russian', '\U0001f310'),
|
('ru', 'Русский', '', 'Russian', '\U0001f310'),
|
||||||
('ru-RU', 'Русский', 'Россия', 'Russian', '\U0001f1f7\U0001f1fa'),
|
('ru-RU', 'Русский', 'Россия', 'Russian', '\U0001f1f7\U0001f1fa'),
|
||||||
('sk', 'Slovenčina', '', 'Slovak', '\U0001f310'),
|
('sk', 'Slovenčina', '', 'Slovak', '\U0001f310'),
|
||||||
('sk-SK', 'Slovenčina', 'Slovensko', 'Slovak', '\U0001f1f8\U0001f1f0'),
|
|
||||||
('sl', 'Slovenščina', '', 'Slovenian', '\U0001f310'),
|
|
||||||
('sl-SI', 'Slovenščina', 'Slovenija', 'Slovenian', '\U0001f1f8\U0001f1ee'),
|
|
||||||
('sq', 'Shqip', '', 'Albanian', '\U0001f310'),
|
('sq', 'Shqip', '', 'Albanian', '\U0001f310'),
|
||||||
('sv', 'Svenska', '', 'Swedish', '\U0001f310'),
|
('sv', 'Svenska', '', 'Swedish', '\U0001f310'),
|
||||||
('sv-FI', 'Svenska', 'Finland', 'Swedish', '\U0001f1eb\U0001f1ee'),
|
('sv-FI', 'Svenska', 'Finland', 'Swedish', '\U0001f1eb\U0001f1ee'),
|
||||||
@@ -112,7 +102,6 @@ sxng_locales = (
|
|||||||
('tr', 'Türkçe', '', 'Turkish', '\U0001f310'),
|
('tr', 'Türkçe', '', 'Turkish', '\U0001f310'),
|
||||||
('tr-TR', 'Türkçe', 'Türkiye', 'Turkish', '\U0001f1f9\U0001f1f7'),
|
('tr-TR', 'Türkçe', 'Türkiye', 'Turkish', '\U0001f1f9\U0001f1f7'),
|
||||||
('uk', 'Українська', '', 'Ukrainian', '\U0001f310'),
|
('uk', 'Українська', '', 'Ukrainian', '\U0001f310'),
|
||||||
('uk-UA', 'Українська', 'Україна', 'Ukrainian', '\U0001f1fa\U0001f1e6'),
|
|
||||||
('vi', 'Tiếng Việt', '', 'Vietnamese', '\U0001f310'),
|
('vi', 'Tiếng Việt', '', 'Vietnamese', '\U0001f310'),
|
||||||
('vi-VN', 'Tiếng Việt', 'Việt Nam', 'Vietnamese', '\U0001f1fb\U0001f1f3'),
|
('vi-VN', 'Tiếng Việt', 'Việt Nam', 'Vietnamese', '\U0001f1fb\U0001f1f3'),
|
||||||
('zh', '中文', '', 'Chinese', '\U0001f310'),
|
('zh', '中文', '', 'Chinese', '\U0001f310'),
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<div id="ai_summary" class="ai-summary hide_if_nojs" {{- ' ' -}}
|
||||||
|
data-query="{{ answer.query }}" {{- ' ' -}}
|
||||||
|
data-grounding="{{ '1' if answer.grounding else '0' }}">{{- '' -}}
|
||||||
|
<div class="ai-summary-header">{{- '' -}}
|
||||||
|
<span class="ai-summary-title">{{ _('AI Summary') }}</span>{{- '' -}}
|
||||||
|
<span class="ai-summary-model"></span>{{- '' -}}
|
||||||
|
</div>{{- '' -}}
|
||||||
|
<div class="ai-summary-body collapsed">{{- '' -}}
|
||||||
|
<div class="ai-summary-answers" aria-live="polite"></div>{{- '' -}}
|
||||||
|
</div>{{- '' -}}
|
||||||
|
<button type="button" class="ai-summary-more invisible" {{- ' ' -}}
|
||||||
|
data-btn-text-collapsed="{{ _('More') }}" {{- ' ' -}}
|
||||||
|
data-btn-text-not-collapsed="{{ _('Less') }}">{{ _('More') }}</button>{{- '' -}}
|
||||||
|
<form class="ai-summary-followup invisible">{{- '' -}}
|
||||||
|
<input type="text" placeholder="{{ _('Ask a follow-up question') }}" autocomplete="off" maxlength="2048">{{- '' -}}
|
||||||
|
<button type="submit">{{ _('Ask') }}</button>{{- '' -}}
|
||||||
|
</form>{{- '' -}}
|
||||||
|
<p class="ai-summary-disclaimer">{{ _('Generated by AI — may contain mistakes.') }}</p>{{- '' -}}
|
||||||
|
</div>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
{%- for output_type in search_formats -%}
|
{%- for output_type in search_formats -%}
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<form method="{{ method }}" action="{{ url_for('search') }}">
|
<form method="{{ method or 'POST' }}" action="{{ url_for('search') }}">
|
||||||
<input type="hidden" name="q" value="{{ q|e }}">
|
<input type="hidden" name="q" value="{{ q|e }}">
|
||||||
{%- for category in selected_categories -%}
|
{%- for category in selected_categories -%}
|
||||||
<input type="hidden" name="category_{{ category }}" value="1">
|
<input type="hidden" name="category_{{ category }}" value="1">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<h4 id="corrections-title">{{ _('Try searching for:') }}</h4>
|
<h4 id="corrections-title">{{ _('Try searching for:') }}</h4>
|
||||||
{% for correction in corrections %}
|
{% for correction in corrections %}
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<form method="{{ method }}" action="{{ url_for('search') }}" role="navigation">
|
<form method="{{ method or 'POST' }}" action="{{ url_for('search') }}" role="navigation">
|
||||||
{% for category in selected_categories %}
|
{% for category in selected_categories %}
|
||||||
<input type="hidden" name="category_{{ category }}" value="1">
|
<input type="hidden" name="category_{{ category }}" value="1">
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<h3><bdi>{{ topic.name }}</bdi></h3>
|
<h3><bdi>{{ topic.name }}</bdi></h3>
|
||||||
{%- for suggestion in topic.suggestions -%}
|
{%- for suggestion in topic.suggestions -%}
|
||||||
<form method="{{ method }}" action="{{ url_for('search') }}">
|
<form method="{{ method or 'POST' }}" action="{{ url_for('search') }}">
|
||||||
<input type="hidden" name="q" value="{{ suggestion }}">
|
<input type="hidden" name="q" value="{{ suggestion }}">
|
||||||
<input type="hidden" name="time_range" value="{{ time_range }}">
|
<input type="hidden" name="time_range" value="{{ time_range }}">
|
||||||
<input type="hidden" name="language" value="{{ current_language }}">
|
<input type="hidden" name="language" value="{{ current_language }}">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<summary class="title" id="suggestions-title">{{ _('Suggestions') }}</summary>
|
<summary class="title" id="suggestions-title">{{ _('Suggestions') }}</summary>
|
||||||
<ul class="wrapper">
|
<ul class="wrapper">
|
||||||
{%- for suggestion in suggestions -%}
|
{%- for suggestion in suggestions -%}
|
||||||
<li><form method="{{ method }}" action="{{ url_for('search') }}">
|
<li><form method="{{ method or 'POST' }}" action="{{ url_for('search') }}">
|
||||||
<input type="hidden" name="q" value="{{ suggestion.url }}">
|
<input type="hidden" name="q" value="{{ suggestion.url }}">
|
||||||
{%- for category in selected_categories -%}
|
{%- for category in selected_categories -%}
|
||||||
<input type="hidden" name="category_{{ category }}" value="1">
|
<input type="hidden" name="category_{{ category }}" value="1">
|
||||||
|
|||||||
@@ -13,9 +13,9 @@
|
|||||||
</Url>
|
</Url>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if autocomplete %}
|
{% if autocomplete %}
|
||||||
<Url rel="suggestions" type="application/x-suggestions+json" method="GET" template="{{ url_for('autocompleter', _external=True) }}?q={searchTerms}"/>
|
<Url rel="suggestions" type="application/x-suggestions+json" method="{{ opensearch_method }}" template="{{ url_for('autocompleter', _external=True) }}?q={searchTerms}"/>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<Url rel="self" type="application/opensearchdescription+xml" template="{{ url_for('opensearch', _external=True) }}" />
|
<Url rel="self" type="application/opensearchdescription+xml" method="{{ opensearch_method }}" template="{{ url_for('opensearch', _external=True) }}" />
|
||||||
<Query role="example" searchTerms="SearXNG" />
|
<Query role="example" searchTerms="SearXNG" />
|
||||||
<moz:SearchForm>{{ url_for('search', _external=True) }}</moz:SearchForm>
|
<moz:SearchForm>{{ url_for('search', _external=True) }}</moz:SearchForm>
|
||||||
</OpenSearchDescription>
|
</OpenSearchDescription>
|
||||||
|
|||||||
@@ -249,6 +249,18 @@
|
|||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{{- tab_footer() -}}
|
{{- tab_footer() -}}
|
||||||
|
|
||||||
|
{# tab: ai #}
|
||||||
|
|
||||||
|
{#- the tab is only shown when the administrator activated the plugin in
|
||||||
|
settings.yml, an instance without an AI summary has no AI tab -#}
|
||||||
|
{%- if 'ai_summary' in plugins_active_by_default
|
||||||
|
and plugins_storage | selectattr('preference_section', 'equalto', 'ai') | list -%}
|
||||||
|
{{- tab_header('maintab', 'ai', _('AI Summary')) -}}
|
||||||
|
{{- plugin_preferences('ai') -}}
|
||||||
|
{%- include 'simple/preferences/ai_summary.html' -%}
|
||||||
|
{{- tab_footer() -}}
|
||||||
|
{%- endif -%}
|
||||||
|
|
||||||
{# tab: cookies #}
|
{# tab: cookies #}
|
||||||
|
|
||||||
{{- tab_header('maintab', 'cookies', _('Cookies')) -}}
|
{{- tab_header('maintab', 'cookies', _('Cookies')) -}}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
{%- if 'ai_summary_server' not in locked_preferences -%}
|
||||||
|
<fieldset>{{- '' -}}
|
||||||
|
<legend id="pref_ai_summary_server">{{- _('AI server URL') -}}</legend>{{- '' -}}
|
||||||
|
<div class="value">{{- '' -}}
|
||||||
|
<input name="ai_summary_server" aria-labelledby="pref_ai_summary_server" type="text"
|
||||||
|
autocomplete="off" spellcheck="false" autocorrect="off"
|
||||||
|
placeholder="{{ ai_summary_default_server or 'http://127.0.0.1:11434' }}"
|
||||||
|
value="{{ preferences.get_value('ai_summary_server') }}">{{- '' -}}
|
||||||
|
</div>{{- '' -}}
|
||||||
|
<div class="description">
|
||||||
|
{{- _('URL of the OpenAI compatible LLM server that generates the summaries (e.g. Ollama, LM Studio, vLLM), e.g. http://192.168.1.10:11434.') -}}
|
||||||
|
{{- ' ' -}}
|
||||||
|
{%- if ai_summary_default_server -%}
|
||||||
|
{{- _('Leave empty to use the default of this instance.') -}}
|
||||||
|
{%- endif -%}
|
||||||
|
</div>{{- '' -}}
|
||||||
|
</fieldset>{{- '' -}}
|
||||||
|
{%- endif -%}
|
||||||
|
{%- if 'ai_summary_api_key' not in locked_preferences -%}
|
||||||
|
<fieldset>{{- '' -}}
|
||||||
|
<legend id="pref_ai_summary_api_key">{{- _('AI server API key') -}}</legend>{{- '' -}}
|
||||||
|
<div class="value">{{- '' -}}
|
||||||
|
<input name="ai_summary_api_key" aria-labelledby="pref_ai_summary_api_key" type="password"
|
||||||
|
autocomplete="off" spellcheck="false" autocorrect="off"
|
||||||
|
value="{{ preferences.get_value('ai_summary_api_key') }}">{{- '' -}}
|
||||||
|
</div>{{- '' -}}
|
||||||
|
<div class="description">
|
||||||
|
{{- _('Optional, only needed if your server requires authentication.') -}}
|
||||||
|
</div>{{- '' -}}
|
||||||
|
</fieldset>{{- '' -}}
|
||||||
|
{%- endif -%}
|
||||||
|
{%- if 'ai_summary_model' not in locked_preferences -%}
|
||||||
|
<fieldset>{{- '' -}}
|
||||||
|
<legend id="pref_ai_summary_model">{{- _('AI summary model') -}}</legend>{{- '' -}}
|
||||||
|
<div class="value">{{- '' -}}
|
||||||
|
<input name="ai_summary_model" aria-labelledby="pref_ai_summary_model" type="text"
|
||||||
|
autocomplete="off" spellcheck="false" autocorrect="off" list="ai_summary_model_list"
|
||||||
|
placeholder="{{ ai_summary_default_model or 'llama3.2:3b' }}"
|
||||||
|
value="{{ preferences.get_value('ai_summary_model') }}">{{- '' -}}
|
||||||
|
<datalist id="ai_summary_model_list">
|
||||||
|
{%- for model in ai_summary_models -%}
|
||||||
|
<option value="{{ model }}"></option>
|
||||||
|
{%- endfor -%}
|
||||||
|
</datalist>{{- '' -}}
|
||||||
|
</div>{{- '' -}}
|
||||||
|
<div class="description">
|
||||||
|
{{- _('Name of the model used to generate the summaries, e.g. llama3.2:3b or gemma3:4b.') -}}
|
||||||
|
{{- ' ' -}}
|
||||||
|
{%- if ai_summary_default_model -%}
|
||||||
|
{{- _('Leave empty to use the default of this instance.') -}}
|
||||||
|
{%- endif -%}
|
||||||
|
</div>{{- '' -}}
|
||||||
|
</fieldset>{{- '' -}}
|
||||||
|
{%- endif -%}
|
||||||
|
{%- if 'ai_summary_grounding' not in locked_preferences -%}
|
||||||
|
<fieldset>{{- '' -}}
|
||||||
|
<legend id="pref_ai_summary_grounding">{{ _('Ground AI summary on search results') }}</legend>{{- '' -}}
|
||||||
|
<p class="value">{{- '' -}}
|
||||||
|
<input type="checkbox" {{- ' ' -}}
|
||||||
|
name="ai_summary_grounding" {{- ' ' -}}
|
||||||
|
aria-labelledby="pref_ai_summary_grounding" {{- ' ' -}}
|
||||||
|
class="checkbox-onoff" {{- ' ' -}}
|
||||||
|
{%- if preferences.get_value('ai_summary_grounding') -%}
|
||||||
|
checked
|
||||||
|
{%- endif -%}{{- ' ' -}}
|
||||||
|
>{{- '' -}}
|
||||||
|
</p>{{- '' -}}
|
||||||
|
<div class="description">
|
||||||
|
{{- _('Send the top search results along with the query, the model answers from this context instead of its own knowledge. Answers are more accurate and more current, but generating them is slower and needs more memory (VRAM) on the Ollama server.') -}}
|
||||||
|
</div>{{- '' -}}
|
||||||
|
</fieldset>{{- '' -}}
|
||||||
|
{%- endif -%}
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
{% if paging %}
|
{% if paging %}
|
||||||
<nav id="pagination" role="navigation">
|
<nav id="pagination" role="navigation">
|
||||||
{% if pageno > 1 %}
|
{% if pageno > 1 %}
|
||||||
<form method="{{ method }}" action="{{ url_for('search') }}" class="previous_page">
|
<form method="{{ method or 'POST' }}" action="{{ url_for('search') }}" class="previous_page">
|
||||||
<div class="{% if rtl %}right{% else %}left{% endif %}">
|
<div class="{% if rtl %}right{% else %}left{% endif %}">
|
||||||
<input type="hidden" name="q" value="{{ q|e }}" >
|
<input type="hidden" name="q" value="{{ q|e }}" >
|
||||||
{% for category in selected_categories %}
|
{% for category in selected_categories %}
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{%- if results | count > 0 -%}
|
{%- if results | count > 0 -%}
|
||||||
<form method="{{ method }}" action="{{ url_for('search') }}" class="next_page">
|
<form method="{{ method or 'POST' }}" action="{{ url_for('search') }}" class="next_page">
|
||||||
<div class="{% if rtl %}left{% else %}right{% endif %}">
|
<div class="{% if rtl %}left{% else %}right{% endif %}">
|
||||||
<input type="hidden" name="q" value="{{ q|e }}" >
|
<input type="hidden" name="q" value="{{ q|e }}" >
|
||||||
{% for category in selected_categories %}
|
{% for category in selected_categories %}
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
|
|
||||||
<div class="numbered_pagination">
|
<div class="numbered_pagination">
|
||||||
{% for x in range(pstart, pend) %}
|
{% for x in range(pstart, pend) %}
|
||||||
<form method="{{ method }}" action="{{ url_for('search') }}" class="page_number">
|
<form method="{{ method or 'POST' }}" action="{{ url_for('search') }}" class="page_number">
|
||||||
<input type="hidden" name="q" value="{{ q|e }}" >
|
<input type="hidden" name="q" value="{{ q|e }}" >
|
||||||
{% for category in selected_categories %}
|
{% for category in selected_categories %}
|
||||||
<input type="hidden" name="category_{{ category }}" value="1" >
|
<input type="hidden" name="category_{{ category }}" value="1" >
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<form id="search" method="{{ method }}" action="{{ url_for('search') }}" role="search">
|
<form id="search" method="{{ method or 'POST' }}" action="{{ url_for('search') }}" role="search">
|
||||||
<div id="search_header">
|
<div id="search_header">
|
||||||
<a id="search_logo" href="{{ url_for('index') }}" tabindex="0" title="{{ _('Display the front page') }}">
|
<a id="search_logo" href="{{ url_for('index') }}" tabindex="0" title="{{ _('Display the front page') }}">
|
||||||
<span hidden>SearXNG</span>
|
<span hidden>SearXNG</span>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<form id="search" method="{{ method }}" action="{{ url_for('search') }}" role="search">
|
<form id="search" method="{{ method or 'POST' }}" action="{{ url_for('search') }}" role="search">
|
||||||
<div id="search_header">
|
<div id="search_header">
|
||||||
<div id="search_view">
|
<div id="search_view">
|
||||||
<div class="search_box">
|
<div class="search_box">
|
||||||
|
|||||||
Binary file not shown.
@@ -50,19 +50,18 @@
|
|||||||
# gallegonovato <gallegonovato@noreply.codeberg.org>, 2025, 2026.
|
# gallegonovato <gallegonovato@noreply.codeberg.org>, 2025, 2026.
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: searx\n"
|
"Project-Id-Version: searx\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2026-07-15 15:45+0000\n"
|
"POT-Creation-Date: 2026-07-15 15:45+0000\n"
|
||||||
"PO-Revision-Date: 2026-07-23 22:07+0000\n"
|
"PO-Revision-Date: 2026-06-14 13:07+0000\n"
|
||||||
"Last-Translator: gallegonovato <gallegonovato@noreply.codeberg.org>\n"
|
"Last-Translator: gallegonovato <gallegonovato@noreply.codeberg.org>\n"
|
||||||
"Language-Team: Spanish <https://translate.codeberg.org/projects/searxng/"
|
|
||||||
"searxng/es/>\n"
|
|
||||||
"Language: es\n"
|
"Language: es\n"
|
||||||
|
"Language-Team: Spanish "
|
||||||
|
"<https://translate.codeberg.org/projects/searxng/searxng/es/>\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=utf-8\n"
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
|
||||||
"X-Generator: Weblate 2026.6.1\n"
|
|
||||||
"Generated-By: Babel 2.18.0\n"
|
"Generated-By: Babel 2.18.0\n"
|
||||||
|
|
||||||
#. CONSTANT_NAMES['NO_SUBGROUPING']
|
#. CONSTANT_NAMES['NO_SUBGROUPING']
|
||||||
@@ -78,7 +77,7 @@ msgstr "otro"
|
|||||||
#. CATEGORY_NAMES['FILES']
|
#. CATEGORY_NAMES['FILES']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "files"
|
msgid "files"
|
||||||
msgstr "archivos"
|
msgstr "files"
|
||||||
|
|
||||||
#. CATEGORY_NAMES['GENERAL']
|
#. CATEGORY_NAMES['GENERAL']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
@@ -2463,3 +2462,4 @@ msgstr "ocultar video"
|
|||||||
|
|
||||||
#~ msgid "Engine"
|
#~ msgid "Engine"
|
||||||
#~ msgstr "Motor"
|
#~ msgstr "Motor"
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -18,19 +18,18 @@
|
|||||||
# return42 <return42@noreply.codeberg.org>, 2025.
|
# return42 <return42@noreply.codeberg.org>, 2025.
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: searx\n"
|
"Project-Id-Version: searx\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2026-07-15 15:45+0000\n"
|
"POT-Creation-Date: 2026-07-15 15:45+0000\n"
|
||||||
"PO-Revision-Date: 2026-07-23 22:07+0000\n"
|
"PO-Revision-Date: 2026-05-01 07:50+0000\n"
|
||||||
"Last-Translator: alexgabi <alexgabi@noreply.codeberg.org>\n"
|
"Last-Translator: alexgabi <alexgabi@noreply.codeberg.org>\n"
|
||||||
"Language-Team: Basque <https://translate.codeberg.org/projects/searxng/"
|
|
||||||
"searxng/eu/>\n"
|
|
||||||
"Language: eu\n"
|
"Language: eu\n"
|
||||||
|
"Language-Team: Basque "
|
||||||
|
"<https://translate.codeberg.org/projects/searxng/searxng/eu/>\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=utf-8\n"
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
|
||||||
"X-Generator: Weblate 2026.6.1\n"
|
|
||||||
"Generated-By: Babel 2.18.0\n"
|
"Generated-By: Babel 2.18.0\n"
|
||||||
|
|
||||||
#. CONSTANT_NAMES['NO_SUBGROUPING']
|
#. CONSTANT_NAMES['NO_SUBGROUPING']
|
||||||
@@ -1440,7 +1439,7 @@ msgstr "Gehienezko denbora"
|
|||||||
|
|
||||||
#: searx/templates/simple/preferences/favicon.html:2
|
#: searx/templates/simple/preferences/favicon.html:2
|
||||||
msgid "Favicon Resolver"
|
msgid "Favicon Resolver"
|
||||||
msgstr "Favicon erabakitzailea"
|
msgstr "Favicon Resolver"
|
||||||
|
|
||||||
#: searx/templates/simple/preferences/favicon.html:15
|
#: searx/templates/simple/preferences/favicon.html:15
|
||||||
msgid "Display favicons near search results"
|
msgid "Display favicons near search results"
|
||||||
@@ -1647,11 +1646,11 @@ msgstr "Ebazpena"
|
|||||||
|
|
||||||
#: searx/templates/simple/result_templates/images.html:55
|
#: searx/templates/simple/result_templates/images.html:55
|
||||||
msgid "Image formats"
|
msgid "Image formats"
|
||||||
msgstr "Irudi formatuak"
|
msgstr ""
|
||||||
|
|
||||||
#: searx/templates/simple/result_templates/images.html:56
|
#: searx/templates/simple/result_templates/images.html:56
|
||||||
msgid "original format"
|
msgid "original format"
|
||||||
msgstr "jatorrizko formatua"
|
msgstr ""
|
||||||
|
|
||||||
#: searx/templates/simple/result_templates/images.html:64
|
#: searx/templates/simple/result_templates/images.html:64
|
||||||
msgid "View source"
|
msgid "View source"
|
||||||
@@ -2395,3 +2394,4 @@ msgstr "ezkutatu bideoa"
|
|||||||
|
|
||||||
#~ msgid "Engine"
|
#~ msgid "Engine"
|
||||||
#~ msgstr "Bilatzailea"
|
#~ msgstr "Bilatzailea"
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -25,23 +25,21 @@
|
|||||||
# return42 <return42@noreply.codeberg.org>, 2025, 2026.
|
# return42 <return42@noreply.codeberg.org>, 2025, 2026.
|
||||||
# ehsanrs2 <ehsanrs2@noreply.codeberg.org>, 2025.
|
# ehsanrs2 <ehsanrs2@noreply.codeberg.org>, 2025.
|
||||||
# Artiman <artiman@noreply.codeberg.org>, 2025.
|
# Artiman <artiman@noreply.codeberg.org>, 2025.
|
||||||
# mvtbh <mvtbh@noreply.codeberg.org>, 2026.
|
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: searx\n"
|
"Project-Id-Version: searx\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2026-07-15 15:45+0000\n"
|
"POT-Creation-Date: 2026-07-15 15:45+0000\n"
|
||||||
"PO-Revision-Date: 2026-08-04 12:25+0000\n"
|
"PO-Revision-Date: 2026-05-19 12:07+0000\n"
|
||||||
"Last-Translator: mvtbh <mvtbh@noreply.codeberg.org>\n"
|
"Last-Translator: return42 <return42@noreply.codeberg.org>\n"
|
||||||
"Language: fa_IR\n"
|
"Language: fa_IR\n"
|
||||||
"Language-Team: Persian <https://translate.codeberg.org/projects/searxng/"
|
"Language-Team: Persian "
|
||||||
"searxng/fa/>\n"
|
"<https://translate.codeberg.org/projects/searxng/searxng/fa/>\n"
|
||||||
"Plural-Forms: nplurals=2; plural=n > 1;\n"
|
"Plural-Forms: nplurals=2; plural=n > 1;\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=utf-8\n"
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Generated-By: Babel 2.18.0\n"
|
"Generated-By: Babel 2.18.0\n"
|
||||||
"X-Generator: Weblate 2026.7.1\n"
|
|
||||||
|
|
||||||
#. CONSTANT_NAMES['NO_SUBGROUPING']
|
#. CONSTANT_NAMES['NO_SUBGROUPING']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
@@ -451,47 +449,47 @@ msgstr ""
|
|||||||
#. WEATHER_CONDITIONS
|
#. WEATHER_CONDITIONS
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "Light snow"
|
msgid "Light snow"
|
||||||
msgstr "برف خفیف"
|
msgstr ""
|
||||||
|
|
||||||
#. WEATHER_CONDITIONS
|
#. WEATHER_CONDITIONS
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "Snow and thunder"
|
msgid "Snow and thunder"
|
||||||
msgstr "برف و رعد و برق"
|
msgstr ""
|
||||||
|
|
||||||
#. WEATHER_CONDITIONS
|
#. WEATHER_CONDITIONS
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "Snow showers and thunder"
|
msgid "Snow showers and thunder"
|
||||||
msgstr "رگبار برف و رعد و برق"
|
msgstr ""
|
||||||
|
|
||||||
#. WEATHER_CONDITIONS
|
#. WEATHER_CONDITIONS
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "Snow showers"
|
msgid "Snow showers"
|
||||||
msgstr "رگبار برف"
|
msgstr ""
|
||||||
|
|
||||||
#. WEATHER_CONDITIONS
|
#. WEATHER_CONDITIONS
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "Snow"
|
msgid "Snow"
|
||||||
msgstr "برف"
|
msgstr ""
|
||||||
|
|
||||||
#. WEATHER_CONDITIONS
|
#. WEATHER_CONDITIONS
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "Heavy snow and thunder"
|
msgid "Heavy snow and thunder"
|
||||||
msgstr "برف سنگین و رعد و برق"
|
msgstr ""
|
||||||
|
|
||||||
#. WEATHER_CONDITIONS
|
#. WEATHER_CONDITIONS
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "Heavy snow showers and thunder"
|
msgid "Heavy snow showers and thunder"
|
||||||
msgstr "رگبار برف سنگین و رعد و برق"
|
msgstr ""
|
||||||
|
|
||||||
#. WEATHER_CONDITIONS
|
#. WEATHER_CONDITIONS
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "Heavy snow showers"
|
msgid "Heavy snow showers"
|
||||||
msgstr "رگبار شدید برف"
|
msgstr ""
|
||||||
|
|
||||||
#. WEATHER_CONDITIONS
|
#. WEATHER_CONDITIONS
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "Heavy snow"
|
msgid "Heavy snow"
|
||||||
msgstr "برف سنگین"
|
msgstr ""
|
||||||
|
|
||||||
#. SOCIAL_MEDIA_TERMS['SUBSCRIBERS']
|
#. SOCIAL_MEDIA_TERMS['SUBSCRIBERS']
|
||||||
#: searx/engines/lemmy.py:85 searx/searxng.msg
|
#: searx/engines/lemmy.py:85 searx/searxng.msg
|
||||||
@@ -2408,3 +2406,4 @@ msgstr "پنهانسازی ویدئو"
|
|||||||
|
|
||||||
#~ msgid "Engine"
|
#~ msgid "Engine"
|
||||||
#~ msgstr "موتور"
|
#~ msgstr "موتور"
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -11,18 +11,18 @@ msgstr ""
|
|||||||
"Project-Id-Version: PROJECT VERSION\n"
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2026-07-15 15:45+0000\n"
|
"POT-Creation-Date: 2026-07-15 15:45+0000\n"
|
||||||
"PO-Revision-Date: 2026-08-04 12:25+0000\n"
|
"PO-Revision-Date: 2026-05-05 12:37+0000\n"
|
||||||
"Last-Translator: Aindriú Mac Giolla Eoin <aindriu80@noreply.codeberg.org>\n"
|
"Last-Translator: Aindriú Mac Giolla Eoin <aindriu80@noreply.codeberg.org>"
|
||||||
|
"\n"
|
||||||
"Language: ga\n"
|
"Language: ga\n"
|
||||||
"Language-Team: Irish <https://translate.codeberg.org/projects/searxng/"
|
"Language-Team: Irish "
|
||||||
"searxng/ga/>\n"
|
"<https://translate.codeberg.org/projects/searxng/searxng/ga/>\n"
|
||||||
"Plural-Forms: nplurals=5; plural=n==1 ? 0 : n==2 ? 1 : (n>2 && n<7) ? 2 :(n>"
|
"Plural-Forms: nplurals=5; plural=n==1 ? 0 : n==2 ? 1 : (n>2 && n<7) ? 2 "
|
||||||
"6 && n<11) ? 3 : 4;\n"
|
":(n>6 && n<11) ? 3 : 4;\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=utf-8\n"
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Generated-By: Babel 2.18.0\n"
|
"Generated-By: Babel 2.18.0\n"
|
||||||
"X-Generator: Weblate 2026.7.1\n"
|
|
||||||
|
|
||||||
#. CONSTANT_NAMES['NO_SUBGROUPING']
|
#. CONSTANT_NAMES['NO_SUBGROUPING']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
@@ -1646,11 +1646,11 @@ msgstr "Réiteach"
|
|||||||
|
|
||||||
#: searx/templates/simple/result_templates/images.html:55
|
#: searx/templates/simple/result_templates/images.html:55
|
||||||
msgid "Image formats"
|
msgid "Image formats"
|
||||||
msgstr "Formáidí íomhá"
|
msgstr ""
|
||||||
|
|
||||||
#: searx/templates/simple/result_templates/images.html:56
|
#: searx/templates/simple/result_templates/images.html:56
|
||||||
msgid "original format"
|
msgid "original format"
|
||||||
msgstr "formáid bhunaidh"
|
msgstr ""
|
||||||
|
|
||||||
#: searx/templates/simple/result_templates/images.html:64
|
#: searx/templates/simple/result_templates/images.html:64
|
||||||
msgid "View source"
|
msgid "View source"
|
||||||
@@ -1969,3 +1969,4 @@ msgstr "físeán a cheilt"
|
|||||||
|
|
||||||
#~ msgid "Engine"
|
#~ msgid "Engine"
|
||||||
#~ msgstr "Inneall"
|
#~ msgstr "Inneall"
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -24,24 +24,22 @@
|
|||||||
# ngf <ngf@noreply.codeberg.org>, 2025.
|
# ngf <ngf@noreply.codeberg.org>, 2025.
|
||||||
# return42 <return42@noreply.codeberg.org>, 2025, 2026.
|
# return42 <return42@noreply.codeberg.org>, 2025, 2026.
|
||||||
# omeritzics <omeritzics@noreply.codeberg.org>, 2026.
|
# omeritzics <omeritzics@noreply.codeberg.org>, 2026.
|
||||||
# ShinyDust <shinydust@noreply.codeberg.org>, 2026.
|
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: searx\n"
|
"Project-Id-Version: searx\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2026-07-15 15:45+0000\n"
|
"POT-Creation-Date: 2026-07-15 15:45+0000\n"
|
||||||
"PO-Revision-Date: 2026-08-02 00:25+0000\n"
|
"PO-Revision-Date: 2026-05-19 12:07+0000\n"
|
||||||
"Last-Translator: ShinyDust <shinydust@noreply.codeberg.org>\n"
|
"Last-Translator: return42 <return42@noreply.codeberg.org>\n"
|
||||||
"Language: he\n"
|
"Language: he\n"
|
||||||
"Language-Team: Hebrew <https://translate.codeberg.org/projects/searxng/"
|
"Language-Team: Hebrew "
|
||||||
"searxng/he/>\n"
|
"<https://translate.codeberg.org/projects/searxng/searxng/he/>\n"
|
||||||
"Plural-Forms: nplurals=4; plural=(n == 1) ? 0 : ((n == 2) ? 1 : ((n > 10 && "
|
"Plural-Forms: nplurals=4; plural=(n == 1) ? 0 : ((n == 2) ? 1 : ((n > 10 "
|
||||||
"n % 10 == 0) ? 2 : 3));\n"
|
"&& n % 10 == 0) ? 2 : 3));\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=utf-8\n"
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Generated-By: Babel 2.18.0\n"
|
"Generated-By: Babel 2.18.0\n"
|
||||||
"X-Generator: Weblate 2026.7.1\n"
|
|
||||||
|
|
||||||
#. CONSTANT_NAMES['NO_SUBGROUPING']
|
#. CONSTANT_NAMES['NO_SUBGROUPING']
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
@@ -396,17 +394,17 @@ msgstr "שלג קל"
|
|||||||
#. WEATHER_CONDITIONS
|
#. WEATHER_CONDITIONS
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "Sleet and thunder"
|
msgid "Sleet and thunder"
|
||||||
msgstr "שלג ורעמים"
|
msgstr ""
|
||||||
|
|
||||||
#. WEATHER_CONDITIONS
|
#. WEATHER_CONDITIONS
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "Sleet showers and thunder"
|
msgid "Sleet showers and thunder"
|
||||||
msgstr "ממטרי שלג ורעמים"
|
msgstr ""
|
||||||
|
|
||||||
#. WEATHER_CONDITIONS
|
#. WEATHER_CONDITIONS
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "Sleet showers"
|
msgid "Sleet showers"
|
||||||
msgstr "ממטרי שלג"
|
msgstr ""
|
||||||
|
|
||||||
#. WEATHER_CONDITIONS
|
#. WEATHER_CONDITIONS
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
@@ -446,7 +444,7 @@ msgstr "ממטרי שלג קלים ורעמים"
|
|||||||
#. WEATHER_CONDITIONS
|
#. WEATHER_CONDITIONS
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "Light snow showers"
|
msgid "Light snow showers"
|
||||||
msgstr "ממטרי שלג קלים"
|
msgstr ""
|
||||||
|
|
||||||
#. WEATHER_CONDITIONS
|
#. WEATHER_CONDITIONS
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
@@ -456,17 +454,17 @@ msgstr "שלג קל"
|
|||||||
#. WEATHER_CONDITIONS
|
#. WEATHER_CONDITIONS
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "Snow and thunder"
|
msgid "Snow and thunder"
|
||||||
msgstr "שלג ורעמים"
|
msgstr ""
|
||||||
|
|
||||||
#. WEATHER_CONDITIONS
|
#. WEATHER_CONDITIONS
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "Snow showers and thunder"
|
msgid "Snow showers and thunder"
|
||||||
msgstr "ממטרי שלג ורעמים"
|
msgstr ""
|
||||||
|
|
||||||
#. WEATHER_CONDITIONS
|
#. WEATHER_CONDITIONS
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
msgid "Snow showers"
|
msgid "Snow showers"
|
||||||
msgstr "ממטרי שלג"
|
msgstr ""
|
||||||
|
|
||||||
#. WEATHER_CONDITIONS
|
#. WEATHER_CONDITIONS
|
||||||
#: searx/searxng.msg
|
#: searx/searxng.msg
|
||||||
@@ -657,7 +655,7 @@ msgstr "חשב {func} של הארגומנטים"
|
|||||||
#: searx/engines/boardreader.py:108
|
#: searx/engines/boardreader.py:108
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Posted by {author}"
|
msgid "Posted by {author}"
|
||||||
msgstr "פורסם על ידי {author}"
|
msgstr ""
|
||||||
|
|
||||||
#: searx/engines/openstreetmap.py:155
|
#: searx/engines/openstreetmap.py:155
|
||||||
msgid "Show route in map .."
|
msgid "Show route in map .."
|
||||||
@@ -745,7 +743,7 @@ msgstr "מחשבון"
|
|||||||
|
|
||||||
#: searx/plugins/calculator.py:26
|
#: searx/plugins/calculator.py:26
|
||||||
msgid "Parses and solves mathematical expressions."
|
msgid "Parses and solves mathematical expressions."
|
||||||
msgstr "מנתח ופותר ביטויים מתמטיים."
|
msgstr ""
|
||||||
|
|
||||||
#: searx/plugins/hash_plugin.py:33
|
#: searx/plugins/hash_plugin.py:33
|
||||||
msgid "Hash plugin"
|
msgid "Hash plugin"
|
||||||
@@ -756,8 +754,6 @@ msgid ""
|
|||||||
"Converts strings to different hash digests. Available functions: md5, "
|
"Converts strings to different hash digests. Available functions: md5, "
|
||||||
"sha1, sha224, sha256, sha384, sha512."
|
"sha1, sha224, sha256, sha384, sha512."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"ממיר מחרוזות ל- hash digests שונים. פונקציות זמינות: md5, sha1, sha224, "
|
|
||||||
"sha256, sha384, sha512."
|
|
||||||
|
|
||||||
#: searx/plugins/hash_plugin.py:63
|
#: searx/plugins/hash_plugin.py:63
|
||||||
msgid "hash digest"
|
msgid "hash digest"
|
||||||
@@ -987,15 +983,14 @@ msgid ""
|
|||||||
"This is a preview of the settings used by the 'Search URL' you used to "
|
"This is a preview of the settings used by the 'Search URL' you used to "
|
||||||
"get here."
|
"get here."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"זוהי תצוגה מקדימה של ההגדרות ב- 'כתובת URL לחיפוש' בהן השתמשת כדי להגיע לכאן."
|
|
||||||
|
|
||||||
#: searx/templates/simple/preferences.html:158
|
#: searx/templates/simple/preferences.html:158
|
||||||
msgid "Press save to copy these preferences to your browser."
|
msgid "Press save to copy these preferences to your browser."
|
||||||
msgstr "לחץ על שמור כדי להעתיק את ההעדפות הללו לדפדפן שלך."
|
msgstr ""
|
||||||
|
|
||||||
#: searx/templates/simple/preferences.html:159
|
#: searx/templates/simple/preferences.html:159
|
||||||
msgid "Click here to view your browser preferences instead:"
|
msgid "Click here to view your browser preferences instead:"
|
||||||
msgstr "לחץ כאן כדי להציג את העדפות הדפדפן שלך במקום:"
|
msgstr ""
|
||||||
|
|
||||||
#: searx/templates/simple/preferences.html:169
|
#: searx/templates/simple/preferences.html:169
|
||||||
msgid "General"
|
msgid "General"
|
||||||
@@ -1338,7 +1333,7 @@ msgstr "השלמה אוטומטית"
|
|||||||
|
|
||||||
#: searx/templates/simple/preferences/autocomplete.html:15
|
#: searx/templates/simple/preferences/autocomplete.html:15
|
||||||
msgid "Show possible queries as you type"
|
msgid "Show possible queries as you type"
|
||||||
msgstr "הצג שאילתות אפשריות תוך כדי הקלדה"
|
msgstr ""
|
||||||
|
|
||||||
#: searx/templates/simple/preferences/center_alignment.html:2
|
#: searx/templates/simple/preferences/center_alignment.html:2
|
||||||
msgid "Center Alignment"
|
msgid "Center Alignment"
|
||||||
@@ -1356,7 +1351,7 @@ msgstr "זוהי רשימת העוגיות וערכיהן אשר SearXNG מאח
|
|||||||
|
|
||||||
#: searx/templates/simple/preferences/cookies.html:3
|
#: searx/templates/simple/preferences/cookies.html:3
|
||||||
msgid "With this list, you can assess the transparency of SearXNG."
|
msgid "With this list, you can assess the transparency of SearXNG."
|
||||||
msgstr "בעזרת רשימה זו, תוכלו להעריך את השקיפות של SearXNG."
|
msgstr ""
|
||||||
|
|
||||||
#: searx/templates/simple/preferences/cookies.html:9
|
#: searx/templates/simple/preferences/cookies.html:9
|
||||||
msgid "Cookie name"
|
msgid "Cookie name"
|
||||||
@@ -1647,11 +1642,11 @@ msgstr "רזולוציה"
|
|||||||
|
|
||||||
#: searx/templates/simple/result_templates/images.html:55
|
#: searx/templates/simple/result_templates/images.html:55
|
||||||
msgid "Image formats"
|
msgid "Image formats"
|
||||||
msgstr "פורמטי תמונה"
|
msgstr ""
|
||||||
|
|
||||||
#: searx/templates/simple/result_templates/images.html:56
|
#: searx/templates/simple/result_templates/images.html:56
|
||||||
msgid "original format"
|
msgid "original format"
|
||||||
msgstr "פורמט מקורי"
|
msgstr ""
|
||||||
|
|
||||||
#: searx/templates/simple/result_templates/images.html:64
|
#: searx/templates/simple/result_templates/images.html:64
|
||||||
msgid "View source"
|
msgid "View source"
|
||||||
@@ -2378,3 +2373,4 @@ msgstr "הסתר וידאו"
|
|||||||
|
|
||||||
#~ msgid "Engine"
|
#~ msgid "Engine"
|
||||||
#~ msgstr "מנוע"
|
#~ msgstr "מנוע"
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user