mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
Merge remote-tracking branch 'upstream/main'
This commit is contained in:
@@ -413,12 +413,14 @@ PicoClaw supports 30+ LLM providers through the `model_list` configuration. Use
|
||||
| [Ollama](https://ollama.com/) | `ollama/` | Not needed | Local models, self-hosted |
|
||||
| [vLLM](https://docs.vllm.ai/) | `vllm/` | Not needed | Local deployment, OpenAI-compatible |
|
||||
| [LiteLLM](https://docs.litellm.ai/) | `litellm/` | Varies | Proxy for 100+ providers |
|
||||
| [Azure OpenAI](https://portal.azure.com/) | `azure/` | Required | Enterprise Azure deployment |
|
||||
| [Azure OpenAI](https://portal.azure.com/) | `azure/` | API key or Entra ID** | Enterprise Azure deployment |
|
||||
| [GitHub Copilot](https://github.com/features/copilot) | `github-copilot/` | OAuth | Device code login |
|
||||
| [Antigravity](https://console.cloud.google.com/) | `antigravity/` | OAuth | Google Cloud AI |
|
||||
| [AWS Bedrock](https://console.aws.amazon.com/bedrock)* | `bedrock/` | AWS credentials | Claude, Llama, Mistral on AWS |
|
||||
|
||||
> \* AWS Bedrock requires build tag: `go build -tags bedrock`. Set `api_base` to a region name (e.g., `us-east-1`) for automatic endpoint resolution across all AWS partitions (aws, aws-cn, aws-us-gov). When using a full endpoint URL instead, you must also configure `AWS_REGION` via environment variable or AWS config/profile.
|
||||
>
|
||||
> \*\* Azure OpenAI uses `api_key` when set. If `api_key` is omitted, the provider falls back to Microsoft Entra ID via `DefaultAzureCredential` (env vars, workload identity, managed identity, Azure CLI, etc.). The Entra ID path requires build tag: `go build -tags azidentity`.
|
||||
|
||||
<details>
|
||||
<summary><b>Local deployment (Ollama, vLLM, etc.)</b></summary>
|
||||
|
||||
@@ -4,6 +4,8 @@ go 1.25.10
|
||||
|
||||
require (
|
||||
fyne.io/systray v1.12.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1
|
||||
github.com/SevereCloud/vksdk/v3 v3.3.1
|
||||
github.com/adhocore/gronx v1.20.0
|
||||
github.com/anthropics/anthropic-sdk-go v1.26.0
|
||||
@@ -55,6 +57,8 @@ require (
|
||||
require (
|
||||
aead.dev/minisign v0.2.0 // indirect
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 // indirect
|
||||
@@ -82,7 +86,9 @@ require (
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
@@ -91,6 +97,7 @@ require (
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/segmentio/asm v1.1.3 // indirect
|
||||
|
||||
@@ -5,6 +5,18 @@ filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
fyne.io/systray v1.12.1 h1:ygBD6aZXwiOmZoY5N+ukbH9pih0Kq6fYgVeMYbr5skQ=
|
||||
fyne.io/systray v1.12.1/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 h1:jHb/wfvRikGdxMXYV3QG/SzUOPYN9KEUUuC0Yd0/vC0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1/go.mod h1:pzBXCYn05zvYIrwLgtK8Ap8QcjRg+0i76tMQdWN6wOk=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY=
|
||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
|
||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||
github.com/SevereCloud/vksdk/v3 v3.3.1 h1:O86zsp5LQnHE+O5acvuXM/s6S1LyxzVTkF6+Lup0Jyg=
|
||||
@@ -164,6 +176,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyf
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
|
||||
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||
@@ -179,6 +193,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/larksuite/oapi-sdk-go/v3 v3.7.5 h1:dimv+ZAGia01f4xCDGvCiBHKWMf4K1AB7fGsM+lv5Jw=
|
||||
github.com/larksuite/oapi-sdk-go/v3 v3.7.5/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI=
|
||||
github.com/line/line-bot-sdk-go/v8 v8.20.0 h1:Jv22DV3JuQ5qZvniqUbg504bJrVzffXs2CMpyoiuIZU=
|
||||
@@ -225,6 +241,8 @@ github.com/pion/rtp v1.10.2 h1:l+f6tTDcAH6xwepaAoW791ddhuYsJlqRATOzirO04Mo=
|
||||
github.com/pion/rtp v1.10.2/go.mod h1:Au8fc6cEByy8RLTwKTQTEeQqDB/SJDxwL4mZuxYA5Pk=
|
||||
github.com/pion/webrtc/v3 v3.3.6 h1:7XAh4RPtlY1Vul6/GmZrv7z+NnxKA6If0KStXBI2ZLE=
|
||||
github.com/pion/webrtc/v3 v3.3.6/go.mod h1:zyN7th4mZpV27eXybfR/cnUf3J2DRy8zw/mdjD9JTNM=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -384,6 +402,7 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
//go:build azidentity
|
||||
|
||||
// Entra ID (DefaultAzureCredential) auth adapter.
|
||||
// Built only when -tags azidentity is supplied; otherwise identity_stub.go
|
||||
// satisfies the same exported API with a friendly error.
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
|
||||
)
|
||||
|
||||
// azureOpenAIScope is the OAuth scope for Azure OpenAI (Cognitive Services).
|
||||
// Service-wide scope, so it covers all regions including sovereign clouds.
|
||||
const azureOpenAIScope = "https://cognitiveservices.azure.com/.default"
|
||||
|
||||
// NewProviderWithIdentity creates an Azure OpenAI provider authenticated via
|
||||
// the DefaultAzureCredential chain (env vars, workload identity, managed
|
||||
// identity, Azure CLI, ...). Construction itself only fails if the credential
|
||||
// chain cannot be built; misconfigured environments surface their error on
|
||||
// the first Chat call when GetToken is invoked.
|
||||
func NewProviderWithIdentity(apiBase, proxy, userAgent string, opts ...Option) (*Provider, error) {
|
||||
cred, err := azidentity.NewDefaultAzureCredential(nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating azure default credential: %w", err)
|
||||
}
|
||||
|
||||
ts := func(ctx context.Context) (string, error) {
|
||||
tok, err := cred.GetToken(ctx, policy.TokenRequestOptions{
|
||||
Scopes: []string{azureOpenAIScope},
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("acquiring azure access token: %w", err)
|
||||
}
|
||||
return tok.Token, nil
|
||||
}
|
||||
|
||||
return NewProviderWithTokenSource(apiBase, proxy, userAgent, ts, opts...), nil
|
||||
}
|
||||
|
||||
// NewProviderWithIdentityAndTimeout mirrors NewProviderWithTimeout for the
|
||||
// identity auth path.
|
||||
func NewProviderWithIdentityAndTimeout(
|
||||
apiBase, proxy, userAgent string,
|
||||
requestTimeoutSeconds int,
|
||||
) (*Provider, error) {
|
||||
return NewProviderWithIdentity(
|
||||
apiBase, proxy, userAgent,
|
||||
WithRequestTimeout(time.Duration(requestTimeoutSeconds)*time.Second),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//go:build !azidentity
|
||||
|
||||
// Stub for the Entra ID auth path when built without the azidentity tag.
|
||||
// Mirrors the exported surface of identity.go so callers compile cleanly
|
||||
// in the default build.
|
||||
|
||||
package azure
|
||||
|
||||
import "fmt"
|
||||
|
||||
const azidentityBuildHint = "azure identity auth not available: build with -tags azidentity to enable Entra ID auth, or set api_key"
|
||||
|
||||
// NewProviderWithIdentity returns an error in the default build.
|
||||
func NewProviderWithIdentity(apiBase, proxy, userAgent string, opts ...Option) (*Provider, error) {
|
||||
return nil, fmt.Errorf("%s", azidentityBuildHint)
|
||||
}
|
||||
|
||||
// NewProviderWithIdentityAndTimeout returns an error in the default build.
|
||||
func NewProviderWithIdentityAndTimeout(
|
||||
apiBase, proxy, userAgent string,
|
||||
requestTimeoutSeconds int,
|
||||
) (*Provider, error) {
|
||||
return nil, fmt.Errorf("%s", azidentityBuildHint)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
//go:build azidentity
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewProviderWithIdentity_Construction(t *testing.T) {
|
||||
// DefaultAzureCredential construction itself does not require any env vars;
|
||||
// failures surface only on the first GetToken call. Verify we get a
|
||||
// non-nil provider back with a token source wired in.
|
||||
p, err := NewProviderWithIdentity("https://example.openai.azure.com", "", "ua-test")
|
||||
if err != nil {
|
||||
t.Fatalf("NewProviderWithIdentity() error = %v", err)
|
||||
}
|
||||
if p == nil {
|
||||
t.Fatal("NewProviderWithIdentity() returned nil provider")
|
||||
}
|
||||
if p.tokenSource == nil {
|
||||
t.Fatal("provider.tokenSource should be set")
|
||||
}
|
||||
if p.apiKey != "" {
|
||||
t.Errorf("provider.apiKey = %q, want empty", p.apiKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewProviderWithIdentityAndTimeout_Construction(t *testing.T) {
|
||||
p, err := NewProviderWithIdentityAndTimeout("https://example.openai.azure.com", "", "ua-test", 30)
|
||||
if err != nil {
|
||||
t.Fatalf("NewProviderWithIdentityAndTimeout() error = %v", err)
|
||||
}
|
||||
if p == nil {
|
||||
t.Fatal("returned nil provider")
|
||||
}
|
||||
if p.httpClient.Timeout.Seconds() != 30 {
|
||||
t.Errorf("timeout = %v, want 30s", p.httpClient.Timeout)
|
||||
}
|
||||
}
|
||||
@@ -33,10 +33,11 @@ const (
|
||||
// It handles Azure-specific authentication (Bearer token), URL construction
|
||||
// (Responses API), and request/response formatting.
|
||||
type Provider struct {
|
||||
apiKey string
|
||||
apiBase string
|
||||
httpClient *http.Client
|
||||
userAgent string
|
||||
apiKey string
|
||||
apiBase string
|
||||
httpClient *http.Client
|
||||
userAgent string
|
||||
tokenSource func(ctx context.Context) (string, error)
|
||||
}
|
||||
|
||||
// Option configures the Azure Provider.
|
||||
@@ -58,6 +59,14 @@ func WithUserAgent(userAgent string) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithTokenSource sets a callback that returns a bearer token per request.
|
||||
// When set, it takes precedence over the static api key.
|
||||
func WithTokenSource(ts func(ctx context.Context) (string, error)) Option {
|
||||
return func(p *Provider) {
|
||||
p.tokenSource = ts
|
||||
}
|
||||
}
|
||||
|
||||
// NewProvider creates a new Azure OpenAI provider.
|
||||
func NewProvider(apiKey, apiBase, proxy, userAgent string, opts ...Option) *Provider {
|
||||
p := &Provider{
|
||||
@@ -84,6 +93,30 @@ func NewProviderWithTimeout(apiKey, apiBase, proxy, userAgent string, requestTim
|
||||
)
|
||||
}
|
||||
|
||||
// NewProviderWithTokenSource creates a new Azure OpenAI provider that obtains its
|
||||
// bearer token from the supplied callback on every request. Used for Entra ID auth
|
||||
// where tokens are short-lived and refreshed by the underlying credential.
|
||||
func NewProviderWithTokenSource(
|
||||
apiBase, proxy, userAgent string,
|
||||
tokenSource func(ctx context.Context) (string, error),
|
||||
opts ...Option,
|
||||
) *Provider {
|
||||
p := &Provider{
|
||||
apiBase: strings.TrimRight(apiBase, "/"),
|
||||
userAgent: userAgent,
|
||||
httpClient: common.NewHTTPClient(proxy),
|
||||
tokenSource: tokenSource,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
if opt != nil {
|
||||
opt(p)
|
||||
}
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// Chat sends a request to the Azure OpenAI Responses API endpoint.
|
||||
// The model parameter is passed in the request body.
|
||||
func (p *Provider) Chat(
|
||||
@@ -147,7 +180,14 @@ func (p *Provider) Chat(
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if p.apiKey != "" {
|
||||
switch {
|
||||
case p.tokenSource != nil:
|
||||
tok, tokErr := p.tokenSource(ctx)
|
||||
if tokErr != nil {
|
||||
return nil, fmt.Errorf("acquiring azure identity token: %w", tokErr)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+tok)
|
||||
case p.apiKey != "":
|
||||
req.Header.Set("Authorization", "Bearer "+p.apiKey)
|
||||
}
|
||||
if p.userAgent != "" {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
@@ -415,3 +417,68 @@ func TestProviderChat_AzureNoNativeWebSearch(t *testing.T) {
|
||||
t.Errorf("tool type = %v, want %q", tool["type"], "function")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderChat_AzureTokenSourceHeader(t *testing.T) {
|
||||
var capturedAuth string
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
capturedAuth = r.Header.Get("Authorization")
|
||||
writeValidResponse(w)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
ts := func(ctx context.Context) (string, error) {
|
||||
return "fake-entra-token", nil
|
||||
}
|
||||
p := NewProviderWithTokenSource(server.URL, "", "", ts)
|
||||
_, err := p.Chat(t.Context(), []Message{{Role: "user", Content: "hi"}}, nil, "deployment", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Chat() error = %v", err)
|
||||
}
|
||||
if capturedAuth != "Bearer fake-entra-token" {
|
||||
t.Errorf("Authorization header = %q, want %q", capturedAuth, "Bearer fake-entra-token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderChat_AzureTokenSourceError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
writeValidResponse(w)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
wantErr := errors.New("creds gone")
|
||||
ts := func(ctx context.Context) (string, error) {
|
||||
return "", wantErr
|
||||
}
|
||||
p := NewProviderWithTokenSource(server.URL, "", "", ts)
|
||||
_, err := p.Chat(t.Context(), []Message{{Role: "user", Content: "hi"}}, nil, "deployment", nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected error from token source")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "creds gone") {
|
||||
t.Errorf("error %q should wrap original token source error", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderChat_AzureTokenSourcePrecedence(t *testing.T) {
|
||||
var capturedAuth string
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
capturedAuth = r.Header.Get("Authorization")
|
||||
writeValidResponse(w)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
ts := func(ctx context.Context) (string, error) {
|
||||
return "from-token-source", nil
|
||||
}
|
||||
// Provider with both an api_key AND a token source: token source must win.
|
||||
p := NewProvider("static-api-key", server.URL, "", "", WithTokenSource(ts))
|
||||
_, err := p.Chat(t.Context(), []Message{{Role: "user", Content: "hi"}}, nil, "deployment", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Chat() error = %v", err)
|
||||
}
|
||||
if capturedAuth != "Bearer from-token-source" {
|
||||
t.Errorf("Authorization header = %q, want token-source value", capturedAuth)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,23 +137,32 @@ func CreateProviderFromConfig(cfg *config.ModelConfig) (LLMProvider, string, err
|
||||
return finalizeProviderFromConfig(provider, modelID, cfg)
|
||||
|
||||
case "azure":
|
||||
// Azure OpenAI uses deployment-based URLs, api-key header auth,
|
||||
// and always sends max_completion_tokens.
|
||||
if cfg.APIKey() == "" {
|
||||
return nil, "", fmt.Errorf("api_key is required for azure protocol")
|
||||
}
|
||||
// Azure OpenAI uses deployment-based URLs. Auth is Bearer token via api_key
|
||||
// when set; otherwise falls back to Entra ID (DefaultAzureCredential).
|
||||
if cfg.APIBase == "" {
|
||||
return nil, "", fmt.Errorf(
|
||||
"api_base is required for azure protocol (e.g., https://your-resource.openai.azure.com)",
|
||||
)
|
||||
}
|
||||
return finalizeProviderFromConfig(azure.NewProviderWithTimeout(
|
||||
cfg.APIKey(),
|
||||
if cfg.APIKey() != "" {
|
||||
return finalizeProviderFromConfig(azure.NewProviderWithTimeout(
|
||||
cfg.APIKey(),
|
||||
cfg.APIBase,
|
||||
cfg.Proxy,
|
||||
userAgent,
|
||||
cfg.RequestTimeout,
|
||||
), modelID, cfg)
|
||||
}
|
||||
provider, err := azure.NewProviderWithIdentityAndTimeout(
|
||||
cfg.APIBase,
|
||||
cfg.Proxy,
|
||||
userAgent,
|
||||
cfg.RequestTimeout,
|
||||
), modelID, cfg)
|
||||
)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return finalizeProviderFromConfig(provider, modelID, cfg)
|
||||
|
||||
case "bedrock":
|
||||
// AWS Bedrock uses AWS SDK credentials (env vars, profiles, IAM roles, etc.)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
//go:build azidentity
|
||||
|
||||
// PicoClaw - Ultra-lightweight personal AI agent
|
||||
// License: MIT
|
||||
//
|
||||
// Copyright (c) 2026 PicoClaw contributors
|
||||
|
||||
package providers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
)
|
||||
|
||||
// With the azidentity build tag, an azure config with no api_key must succeed
|
||||
// (falls back to DefaultAzureCredential). Construction does not require any
|
||||
// real Azure environment — token acquisition happens on first Chat.
|
||||
func TestCreateProviderFromConfig_AzureIdentityFallback(t *testing.T) {
|
||||
cfg := &config.ModelConfig{
|
||||
ModelName: "azure-gpt5",
|
||||
Model: "azure/my-gpt5-deployment",
|
||||
APIBase: "https://my-resource.openai.azure.com",
|
||||
}
|
||||
|
||||
provider, modelID, err := CreateProviderFromConfig(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateProviderFromConfig() error = %v", err)
|
||||
}
|
||||
if provider == nil {
|
||||
t.Fatal("CreateProviderFromConfig() returned nil provider")
|
||||
}
|
||||
if modelID != "my-gpt5-deployment" {
|
||||
t.Errorf("modelID = %q, want %q", modelID, "my-gpt5-deployment")
|
||||
}
|
||||
}
|
||||
@@ -870,8 +870,11 @@ func TestCreateProviderFromConfig_AzureMissingAPIKey(t *testing.T) {
|
||||
}
|
||||
|
||||
_, _, err := CreateProviderFromConfig(cfg)
|
||||
if err == nil {
|
||||
t.Fatal("CreateProviderFromConfig() expected error for missing API key")
|
||||
// Without api_key the factory falls back to identity auth, which in the
|
||||
// default build is stubbed out and surfaces a build-tag error. With the
|
||||
// azidentity tag, the call succeeds and is covered by a separate test.
|
||||
if err != nil && !strings.Contains(err.Error(), "azidentity") {
|
||||
t.Fatalf("CreateProviderFromConfig() unexpected error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -291,6 +291,9 @@ export function AppHeader() {
|
||||
<DropdownMenuItem onClick={() => i18n.changeLanguage("pt-BR")}>
|
||||
Português (Brasil)
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => i18n.changeLanguage("bn-IN")}>
|
||||
বাংলা
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => i18n.changeLanguage("zh")}>
|
||||
简体中文
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { IconArrowUp, IconPhotoPlus, IconX } from "@tabler/icons-react"
|
||||
import { useRef, type KeyboardEvent as ReactKeyboardEvent } from "react"
|
||||
import {
|
||||
type ClipboardEvent as ReactClipboardEvent,
|
||||
type DragEvent as ReactDragEvent,
|
||||
type KeyboardEvent as ReactKeyboardEvent,
|
||||
useRef,
|
||||
} from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import TextareaAutosize from "react-textarea-autosize"
|
||||
|
||||
@@ -30,11 +35,17 @@ interface ChatComposerProps {
|
||||
attachments: ChatAttachment[]
|
||||
onInputChange: (value: string) => void
|
||||
onAddImages: () => void
|
||||
onPaste: (event: ReactClipboardEvent<HTMLTextAreaElement>) => void
|
||||
onDragEnter: (event: ReactDragEvent<HTMLDivElement>) => void
|
||||
onDragLeave: (event: ReactDragEvent<HTMLDivElement>) => void
|
||||
onDragOver: (event: ReactDragEvent<HTMLDivElement>) => void
|
||||
onDrop: (event: ReactDragEvent<HTMLDivElement>) => void
|
||||
onRemoveAttachment: (index: number) => void
|
||||
onSend: () => void
|
||||
onContextDetail?: () => void
|
||||
inputDisabledReason: ChatInputDisabledReason | null
|
||||
canSend: boolean
|
||||
isDragActive: boolean
|
||||
contextUsage?: ContextUsage
|
||||
}
|
||||
|
||||
@@ -43,11 +54,17 @@ export function ChatComposer({
|
||||
attachments,
|
||||
onInputChange,
|
||||
onAddImages,
|
||||
onPaste,
|
||||
onDragEnter,
|
||||
onDragLeave,
|
||||
onDragOver,
|
||||
onDrop,
|
||||
onRemoveAttachment,
|
||||
onSend,
|
||||
onContextDetail,
|
||||
inputDisabledReason,
|
||||
canSend,
|
||||
isDragActive,
|
||||
contextUsage,
|
||||
}: ChatComposerProps) {
|
||||
const { t } = useTranslation()
|
||||
@@ -78,8 +95,25 @@ export function ChatComposer({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="before:bg-background pointer-events-none relative z-10 -mt-[24px] shrink-0 overflow-y-auto px-4 pb-[calc(1rem+env(safe-area-inset-bottom))] [scrollbar-gutter:stable] before:pointer-events-none before:absolute before:inset-x-0 before:top-[24px] before:bottom-0 before:content-[''] md:px-8 md:pb-8 lg:px-24 xl:px-48">
|
||||
<div className="bg-card border-border/60 pointer-events-auto relative mx-auto flex max-w-[1000px] flex-col rounded-2xl border p-3 shadow-sm">
|
||||
<div className="before:bg-background pointer-events-none relative z-10 -mt-[24px] shrink-0 [scrollbar-gutter:stable] overflow-y-auto px-4 pb-[calc(1rem+env(safe-area-inset-bottom))] before:pointer-events-none before:absolute before:inset-x-0 before:top-[24px] before:bottom-0 before:content-[''] md:px-8 md:pb-8 lg:px-24 xl:px-48">
|
||||
<div
|
||||
className={cn(
|
||||
"bg-card border-border/60 pointer-events-auto relative mx-auto flex max-w-[1000px] flex-col rounded-2xl border p-3 shadow-sm transition-colors",
|
||||
isDragActive && "border-violet-400/70 bg-violet-500/5",
|
||||
)}
|
||||
onDragEnter={onDragEnter}
|
||||
onDragLeave={onDragLeave}
|
||||
onDragOver={onDragOver}
|
||||
onDrop={onDrop}
|
||||
>
|
||||
{isDragActive && (
|
||||
<div className="pointer-events-none absolute inset-0 z-10 flex items-center justify-center rounded-2xl border-2 border-dashed border-violet-400/70 bg-violet-500/10">
|
||||
<div className="bg-background/95 text-foreground rounded-full px-4 py-2 text-sm font-medium shadow-sm">
|
||||
{t("chat.dropImagesActive")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{attachments.length > 0 && (
|
||||
<div className="mb-3 flex flex-wrap gap-2 px-2">
|
||||
{attachments.map((attachment, index) => (
|
||||
@@ -115,6 +149,7 @@ export function ChatComposer({
|
||||
onCompositionEnd={() => {
|
||||
composingRef.current = false
|
||||
}}
|
||||
onPaste={onPaste}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
disabled={!canInput}
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { IconPlus } from "@tabler/icons-react"
|
||||
import { useAtom } from "jotai"
|
||||
import { type ChangeEvent, useEffect, useRef, useState } from "react"
|
||||
import {
|
||||
type ChangeEvent,
|
||||
type ClipboardEvent,
|
||||
type DragEvent,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { AssistantMessage } from "@/components/chat/assistant-message"
|
||||
import {
|
||||
@@ -23,6 +29,12 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import {
|
||||
CHAT_IMAGE_ACCEPT,
|
||||
buildChatImageAttachments,
|
||||
getTransferredFiles,
|
||||
hasFileTransfer,
|
||||
} from "@/features/chat/image-input"
|
||||
import { useChatModels } from "@/hooks/use-chat-models"
|
||||
import { useGateway } from "@/hooks/use-gateway"
|
||||
import { usePicoChat } from "@/hooks/use-pico-chat"
|
||||
@@ -36,32 +48,6 @@ import {
|
||||
} from "@/store/chat"
|
||||
import type { GatewayState } from "@/store/gateway"
|
||||
|
||||
const MAX_IMAGE_SIZE_BYTES = 7 * 1024 * 1024
|
||||
const MAX_IMAGE_SIZE_LABEL = "7 MB"
|
||||
const ALLOWED_IMAGE_TYPES = new Set([
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
"image/bmp",
|
||||
])
|
||||
|
||||
function readFileAsDataUrl(file: File): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
if (typeof reader.result === "string") {
|
||||
resolve(reader.result)
|
||||
return
|
||||
}
|
||||
reject(new Error("Failed to read file"))
|
||||
}
|
||||
reader.onerror = () =>
|
||||
reject(reader.error || new Error("Failed to read file"))
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
}
|
||||
|
||||
function resolveChatInputDisabledReason({
|
||||
hasDefaultModel,
|
||||
connectionState,
|
||||
@@ -118,10 +104,12 @@ export function ChatPage() {
|
||||
const { t } = useTranslation()
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const dragDepthRef = useRef(0)
|
||||
const [isAtBottom, setIsAtBottom] = useState(true)
|
||||
const [hasScrolled, setHasScrolled] = useState(false)
|
||||
const [input, setInput] = useState("")
|
||||
const [attachments, setAttachments] = useState<ChatAttachment[]>([])
|
||||
const [isDragActive, setIsDragActive] = useState(false)
|
||||
const [assistantDetailVisibility, setAssistantDetailVisibility] = useAtom(
|
||||
assistantDetailVisibilityAtom,
|
||||
)
|
||||
@@ -223,6 +211,19 @@ export function ChatPage() {
|
||||
setAttachments((prev) => prev.filter((_, itemIndex) => itemIndex !== index))
|
||||
}
|
||||
|
||||
const appendImageFiles = async (files: readonly File[]) => {
|
||||
if (!canInput || files.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const nextAttachments = await buildChatImageAttachments(files, t)
|
||||
if (nextAttachments.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
setAttachments((prev) => [...prev, ...nextAttachments])
|
||||
}
|
||||
|
||||
const handleImageSelection = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(event.target.files ?? [])
|
||||
event.target.value = ""
|
||||
@@ -231,45 +232,77 @@ export function ChatPage() {
|
||||
return
|
||||
}
|
||||
|
||||
const nextAttachments: ChatAttachment[] = []
|
||||
for (const file of files) {
|
||||
if (!ALLOWED_IMAGE_TYPES.has(file.type)) {
|
||||
toast.error(
|
||||
t("chat.invalidImage", {
|
||||
name: file.name,
|
||||
}),
|
||||
)
|
||||
continue
|
||||
}
|
||||
await appendImageFiles(files)
|
||||
}
|
||||
|
||||
if (file.size > MAX_IMAGE_SIZE_BYTES) {
|
||||
toast.error(
|
||||
t("chat.imageTooLarge", {
|
||||
name: file.name,
|
||||
size: MAX_IMAGE_SIZE_LABEL,
|
||||
}),
|
||||
)
|
||||
continue
|
||||
}
|
||||
const resetDragState = () => {
|
||||
dragDepthRef.current = 0
|
||||
setIsDragActive(false)
|
||||
}
|
||||
|
||||
try {
|
||||
nextAttachments.push({
|
||||
type: "image",
|
||||
filename: file.name,
|
||||
url: await readFileAsDataUrl(file),
|
||||
})
|
||||
} catch {
|
||||
toast.error(
|
||||
t("chat.imageReadFailed", {
|
||||
name: file.name,
|
||||
}),
|
||||
)
|
||||
}
|
||||
const handleComposerPaste = async (
|
||||
event: ClipboardEvent<HTMLTextAreaElement>,
|
||||
) => {
|
||||
const files = getTransferredFiles(event.clipboardData)
|
||||
if (files.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (nextAttachments.length > 0) {
|
||||
setAttachments(nextAttachments.slice(0, 1))
|
||||
await appendImageFiles(files)
|
||||
}
|
||||
|
||||
const handleComposerDragEnter = (event: DragEvent<HTMLDivElement>) => {
|
||||
if (!hasFileTransfer(event.dataTransfer)) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
if (!canInput) {
|
||||
return
|
||||
}
|
||||
dragDepthRef.current += 1
|
||||
setIsDragActive(true)
|
||||
}
|
||||
|
||||
const handleComposerDragLeave = (event: DragEvent<HTMLDivElement>) => {
|
||||
if (!hasFileTransfer(event.dataTransfer)) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
if (!canInput) {
|
||||
resetDragState()
|
||||
return
|
||||
}
|
||||
dragDepthRef.current = Math.max(0, dragDepthRef.current - 1)
|
||||
if (dragDepthRef.current === 0) {
|
||||
setIsDragActive(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleComposerDragOver = (event: DragEvent<HTMLDivElement>) => {
|
||||
if (!hasFileTransfer(event.dataTransfer)) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.dataTransfer.dropEffect = canInput ? "copy" : "none"
|
||||
}
|
||||
|
||||
const handleComposerDrop = async (event: DragEvent<HTMLDivElement>) => {
|
||||
if (!hasFileTransfer(event.dataTransfer)) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
const files = getTransferredFiles(event.dataTransfer)
|
||||
resetDragState()
|
||||
|
||||
if (!canInput || files.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
await appendImageFiles(files)
|
||||
}
|
||||
|
||||
const canSubmit =
|
||||
@@ -398,7 +431,8 @@ export function ChatPage() {
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/jpeg,image/png,image/gif,image/webp,image/bmp"
|
||||
accept={CHAT_IMAGE_ACCEPT}
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={handleImageSelection}
|
||||
/>
|
||||
@@ -408,6 +442,11 @@ export function ChatPage() {
|
||||
attachments={attachments}
|
||||
onInputChange={setInput}
|
||||
onAddImages={handleAddImages}
|
||||
onPaste={handleComposerPaste}
|
||||
onDragEnter={handleComposerDragEnter}
|
||||
onDragLeave={handleComposerDragLeave}
|
||||
onDragOver={handleComposerDragOver}
|
||||
onDrop={handleComposerDrop}
|
||||
onRemoveAttachment={handleRemoveAttachment}
|
||||
onSend={handleSend}
|
||||
onContextDetail={() => {
|
||||
@@ -417,6 +456,7 @@ export function ChatPage() {
|
||||
}}
|
||||
inputDisabledReason={inputDisabledReason}
|
||||
canSend={canSubmit}
|
||||
isDragActive={isDragActive}
|
||||
contextUsage={contextUsage}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -39,7 +39,7 @@ export function UserMessage({
|
||||
<img
|
||||
key={`${attachment.url}-${index}`}
|
||||
src={attachment.url}
|
||||
alt={attachment.filename || "Uploaded image"}
|
||||
alt={attachment.filename || t("chat.uploadedImage")}
|
||||
className="max-h-72 max-w-full object-cover"
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import type { TFunction } from "i18next"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import type { ChatAttachment } from "@/store/chat"
|
||||
|
||||
const CHAT_IMAGE_MIME_TYPES = [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
"image/bmp",
|
||||
] as const
|
||||
|
||||
const CHAT_IMAGE_MIME_TYPE_SET = new Set<string>(CHAT_IMAGE_MIME_TYPES)
|
||||
const CHAT_IMAGE_EXTENSION_BY_MIME: Record<string, string> = {
|
||||
"image/jpeg": ".jpg",
|
||||
"image/png": ".png",
|
||||
"image/gif": ".gif",
|
||||
"image/webp": ".webp",
|
||||
"image/bmp": ".bmp",
|
||||
}
|
||||
const CHAT_IMAGE_MIME_BY_EXTENSION: Record<string, string> = {
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".png": "image/png",
|
||||
".gif": "image/gif",
|
||||
".webp": "image/webp",
|
||||
".bmp": "image/bmp",
|
||||
}
|
||||
|
||||
export const CHAT_IMAGE_ACCEPT = CHAT_IMAGE_MIME_TYPES.join(",")
|
||||
|
||||
const MAX_CHAT_IMAGE_SIZE_BYTES = 7 * 1024 * 1024
|
||||
const MAX_CHAT_IMAGE_SIZE_LABEL = "7 MB"
|
||||
|
||||
function readFileAsDataUrl(file: File): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
if (typeof reader.result === "string") {
|
||||
resolve(reader.result)
|
||||
return
|
||||
}
|
||||
reject(new Error("Failed to read file"))
|
||||
}
|
||||
reader.onerror = () =>
|
||||
reject(reader.error || new Error("Failed to read file"))
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
}
|
||||
|
||||
function getFileExtension(fileName: string): string {
|
||||
const lastDotIndex = fileName.lastIndexOf(".")
|
||||
if (lastDotIndex === -1) {
|
||||
return ""
|
||||
}
|
||||
return fileName.slice(lastDotIndex).toLowerCase()
|
||||
}
|
||||
|
||||
function getSupportedImageMimeType(file: File): string | null {
|
||||
const normalizedType = file.type.trim().toLowerCase()
|
||||
if (normalizedType && CHAT_IMAGE_MIME_TYPE_SET.has(normalizedType)) {
|
||||
return normalizedType
|
||||
}
|
||||
|
||||
const extension = getFileExtension(file.name)
|
||||
return CHAT_IMAGE_MIME_BY_EXTENSION[extension] ?? null
|
||||
}
|
||||
|
||||
function normalizeImageFileForDataUrl(file: File, filename: string): File {
|
||||
const mimeType = getSupportedImageMimeType(file)
|
||||
if (!mimeType || file.type.trim().toLowerCase() === mimeType) {
|
||||
return file
|
||||
}
|
||||
|
||||
const normalizedName = file.name.trim() || filename
|
||||
return new File([file], normalizedName, { type: mimeType })
|
||||
}
|
||||
|
||||
function getAttachmentFilename(file: File, index: number): string {
|
||||
const trimmedName = file.name.trim()
|
||||
if (trimmedName) {
|
||||
return trimmedName
|
||||
}
|
||||
|
||||
const mimeType = getSupportedImageMimeType(file)
|
||||
const extension = mimeType ? CHAT_IMAGE_EXTENSION_BY_MIME[mimeType] : ".png"
|
||||
return `image-${index + 1}${extension}`
|
||||
}
|
||||
|
||||
function getTransferItemFiles(dataTransfer: DataTransfer | null): File[] {
|
||||
if (!dataTransfer) {
|
||||
return []
|
||||
}
|
||||
|
||||
const files = Array.from(dataTransfer.files)
|
||||
if (files.length > 0) {
|
||||
return files
|
||||
}
|
||||
|
||||
return Array.from(dataTransfer.items)
|
||||
.filter((item) => item.kind === "file")
|
||||
.map((item) => item.getAsFile())
|
||||
.filter((file): file is File => file !== null)
|
||||
}
|
||||
|
||||
export function hasFileTransfer(dataTransfer: DataTransfer | null): boolean {
|
||||
if (!dataTransfer) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (dataTransfer.files.length > 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
return Array.from(dataTransfer.items).some((item) => item.kind === "file")
|
||||
}
|
||||
|
||||
export function getTransferredFiles(dataTransfer: DataTransfer | null) {
|
||||
return getTransferItemFiles(dataTransfer)
|
||||
}
|
||||
|
||||
export async function buildChatImageAttachments(
|
||||
files: readonly File[],
|
||||
t: TFunction,
|
||||
): Promise<ChatAttachment[]> {
|
||||
const nextAttachments: ChatAttachment[] = []
|
||||
|
||||
for (const [index, file] of files.entries()) {
|
||||
const filename = getAttachmentFilename(file, index)
|
||||
|
||||
const mimeType = getSupportedImageMimeType(file)
|
||||
if (!mimeType) {
|
||||
toast.error(
|
||||
t("chat.invalidImage", {
|
||||
name: filename,
|
||||
}),
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
if (file.size > MAX_CHAT_IMAGE_SIZE_BYTES) {
|
||||
toast.error(
|
||||
t("chat.imageTooLarge", {
|
||||
name: filename,
|
||||
size: MAX_CHAT_IMAGE_SIZE_LABEL,
|
||||
}),
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const normalizedFile = normalizeImageFileForDataUrl(file, filename)
|
||||
nextAttachments.push({
|
||||
type: "image",
|
||||
filename,
|
||||
url: await readFileAsDataUrl(normalizedFile),
|
||||
contentType: mimeType,
|
||||
})
|
||||
} catch {
|
||||
toast.error(
|
||||
t("chat.imageReadFailed", {
|
||||
name: filename,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nextAttachments
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import dayjs from "dayjs"
|
||||
import "dayjs/locale/bn"
|
||||
import "dayjs/locale/cs"
|
||||
import "dayjs/locale/en"
|
||||
import "dayjs/locale/pt-br"
|
||||
@@ -11,6 +12,7 @@ import { initReactI18next } from "react-i18next"
|
||||
|
||||
import en from "./locales/en.json"
|
||||
import ptBr from "./locales/pt-br.json"
|
||||
import bnIn from "./locales/bn-in.json"
|
||||
import zh from "./locales/zh.json"
|
||||
import cs from "./locales/cs.json"
|
||||
|
||||
@@ -33,6 +35,9 @@ i18n
|
||||
"pt-BR": {
|
||||
translation: ptBr,
|
||||
},
|
||||
"bn-IN": {
|
||||
translation: bnIn,
|
||||
},
|
||||
zh: {
|
||||
translation: zh,
|
||||
},
|
||||
@@ -53,6 +58,8 @@ i18n.on("languageChanged", (lng) => {
|
||||
dayjs.locale("zh-cn")
|
||||
} else if (lng.startsWith("pt")) {
|
||||
dayjs.locale("pt-br")
|
||||
} else if (lng.startsWith("bn")) {
|
||||
dayjs.locale("bn")
|
||||
} else if (lng.startsWith("cs")) {
|
||||
dayjs.locale("cs")
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,960 @@
|
||||
{
|
||||
"navigation": {
|
||||
"chat": "চ্যাট",
|
||||
"model_group": "মডেল",
|
||||
"models": "মডেল",
|
||||
"credentials": "ক্রেডেনশিয়াল",
|
||||
"agent_group": "এজেন্ট",
|
||||
"hub": "Hub",
|
||||
"skills": "দক্ষতা",
|
||||
"tools": "সরঞ্জাম",
|
||||
"services": "সার্ভিস",
|
||||
"channels_group": "চ্যানেল",
|
||||
"show_more_channels": "আরও",
|
||||
"show_less_channels": "কম",
|
||||
"config": "কনফিগ",
|
||||
"logs": "লগ"
|
||||
},
|
||||
"launcherLogin": {
|
||||
"title": "সাইন ইন",
|
||||
"description": "চালিয়ে যেতে ড্যাশবোর্ড পাসওয়ার্ড দিন।",
|
||||
"passwordLabel": "পাসওয়ার্ড",
|
||||
"passwordPlaceholder": "পাসওয়ার্ড লিখুন",
|
||||
"submit": "সাইন ইন",
|
||||
"errorInvalid": "ভুল পাসওয়ার্ড। আবার চেষ্টা করুন।",
|
||||
"errorNetwork": "নেটওয়ার্ক ত্রুটি। আবার চেষ্টা করুন।"
|
||||
},
|
||||
"launcherSetup": {
|
||||
"title": "ড্যাশবোর্ড পাসওয়ার্ড সেট করুন",
|
||||
"description": "এই ড্যাশবোর্ডে অ্যাক্সেস সুরক্ষিত রাখতে একটি পাসওয়ার্ড বেছে নিন। প্রতিবার সাইন ইনের সময় এটি ব্যবহার করতে হবে।",
|
||||
"passwordLabel": "পাসওয়ার্ড",
|
||||
"passwordPlaceholder": "কমপক্ষে ৮ অক্ষর",
|
||||
"confirmLabel": "পাসওয়ার্ড নিশ্চিত করুন",
|
||||
"confirmPlaceholder": "পাসওয়ার্ড আবার লিখুন",
|
||||
"submit": "পাসওয়ার্ড সেট করুন",
|
||||
"errorMismatch": "পাসওয়ার্ড মিলছে না।",
|
||||
"errorNetwork": "নেটওয়ার্ক ত্রুটি। আবার চেষ্টা করুন।"
|
||||
},
|
||||
"chat": {
|
||||
"welcome": "আজ আমি আপনাকে কীভাবে সাহায্য করতে পারি?",
|
||||
"welcomeDesc": "আবহাওয়া, সেটিংস বা অন্য যেকোনো কাজ সম্পর্কে আমাকে জিজ্ঞাসা করুন। আমি সাহায্যের জন্য এখানে আছি।",
|
||||
"placeholder": "একটি নতুন বার্তা শুরু করুন...",
|
||||
"disabledPlaceholder": {
|
||||
"gatewayUnknown": "চ্যাট করা যাচ্ছে না: গেটওয়ের স্ট্যাটাস এখনও পরীক্ষা করা হচ্ছে। অনুগ্রহ করে অপেক্ষা করুন, তারপর পৃষ্ঠাটি রিফ্রেশ করুন অথবা প্রয়োজনে লঞ্চার পুনরায় চালু করুন।",
|
||||
"gatewayStarting": "চ্যাট করা যাচ্ছে না: গেটওয়ে চালু হচ্ছে। চালু হওয়া শেষ হওয়া পর্যন্ত অপেক্ষা করুন, তারপর আবার চেষ্টা করুন।",
|
||||
"gatewayRestarting": "চ্যাট করা যাচ্ছে না: গেটওয়ে পুনরায় চালু হচ্ছে। অনুগ্রহ করে পুনরায় চালু হওয়া শেষ হওয়া পর্যন্ত অপেক্ষা করুন।",
|
||||
"gatewayStopping": "চ্যাট করা যাচ্ছে না: গেটওয়ে বন্ধ হচ্ছে। বন্ধ হওয়া পর্যন্ত অপেক্ষা করুন, তারপর গেটওয়ে আবার চালু করুন।",
|
||||
"gatewayStopped": "চ্যাট করা যাচ্ছে না: গেটওয়ে শুরু করা হয়নি। উপরের বারে স্টার্ট গেটওয়েতে ক্লিক করুন, তারপর পুনরায় চেষ্টা করুন।",
|
||||
"gatewayError": "চ্যাট করা যাচ্ছে না: গেটওয়ে ত্রুটি অবস্থায় রয়েছে। লগ পরীক্ষা করুন, তারপর গেটওয়ে বা লঞ্চার পুনরায় চালু করুন।",
|
||||
"websocketConnecting": "চ্যাট সার্ভিসের সাথে সংযোগ স্থাপন হচ্ছে... অনুগ্রহ করে অপেক্ষা করুন।",
|
||||
"websocketDisconnected": "চ্যাট করা যাচ্ছে না: WebSocket সংযোগ বিচ্ছিন্ন। নেটওয়ার্ক এবং গেটওয়ের অবস্থা পরীক্ষা করুন, তারপর পৃষ্ঠাটি রিফ্রেশ করুন বা লঞ্চার পুনরায় চালু করুন।",
|
||||
"websocketError": "চ্যাট করা যাচ্ছে না: WebSocket সংযোগ ব্যর্থ হয়েছে। নেটওয়ার্ক এবং গেটওয়ের অবস্থা পরীক্ষা করুন, তারপর পুনরায় চেষ্টা করুন।",
|
||||
"noDefaultModel": "চ্যাট করা যাচ্ছে না: কোনো ডিফল্ট মডেল নির্বাচিত নেই। মডেল পৃষ্ঠায় একটি ডিফল্ট মডেল সেট করুন।"
|
||||
},
|
||||
"newChat": "নতুন চ্যাট",
|
||||
"notConnected": "গেটওয়ে চলছে না। চ্যাট করতে এটি চালু করুন।",
|
||||
"thinking": {
|
||||
"step1": "চিন্তা করা হচ্ছে...",
|
||||
"step2": "আপনার অনুরোধ বিশ্লেষণ করা হচ্ছে...",
|
||||
"step3": "উত্তর প্রস্তুত করা হচ্ছে...",
|
||||
"step4": "প্রায় শেষ..."
|
||||
},
|
||||
"reasoningLabel": "যুক্তি",
|
||||
"toolCallsLabel": "টুল কল",
|
||||
"toolCallExplanationLabel": "কল নোট",
|
||||
"toolCallFunctionLabel": "কল সারাংশ",
|
||||
"toolCallArgumentsLabel": "আর্গুমেন্ট",
|
||||
"showAssistantDetails": "যুক্তি এবং টুল কল",
|
||||
"assistantDetailVisibility": {
|
||||
"none": "কোনোটিই দেখাবেন না",
|
||||
"thought": "শুধু যুক্তি দেখান",
|
||||
"toolCalls": "শুধু টুল কল দেখান",
|
||||
"all": "উভয়ই দেখান"
|
||||
},
|
||||
"toolLabel": "টুল",
|
||||
"codeLabel": "কোড",
|
||||
"copyMessage": "বার্তা কপি করুন",
|
||||
"copyCode": "কোড কপি করুন",
|
||||
"copiedLabel": "কপি করা হয়েছে",
|
||||
"enableCodeWrap": "লাইন র্যাপ করুন",
|
||||
"disableCodeWrap": "র্যাপ নিষ্ক্রিয় করুন",
|
||||
"expandCode": "কোড প্রসারিত করুন",
|
||||
"collapseCode": "কোড সংকুচিত করুন",
|
||||
"history": "ইতিহাস",
|
||||
"noHistory": "এখনও কোনো চ্যাট ইতিহাস নেই",
|
||||
"historyLoadFailed": "চ্যাট ইতিহাস লোড করতে ব্যর্থ",
|
||||
"historyOpenFailed": "এই চ্যাট ইতিহাস খুলতে ব্যর্থ",
|
||||
"loadingMore": "আরও লোড হচ্ছে...",
|
||||
"deleteSession": "সেশন মুছুন",
|
||||
"messagesCount": "{{count}}টি বার্তা",
|
||||
"noModel": "মডেল নির্বাচন করুন",
|
||||
"inputDisabled": {
|
||||
"notConnected": "গেটওয়ে চলছে না। চ্যাট করতে এটি চালু করুন।",
|
||||
"noModel": "কোনো ডিফল্ট মডেল কনফিগার করা নেই। একটি সেট করতে মডেল পৃষ্ঠায় যান।"
|
||||
},
|
||||
"sendMessage": "বার্তা পাঠান",
|
||||
"sendHint": "পাঠাতে Enter চাপুন\nনতুন লাইনের জন্য Shift + Enter",
|
||||
"contextTitle": "প্রসঙ্গ",
|
||||
"contextDetail": "বিস্তারিত দেখুন",
|
||||
"attachImage": "ছবি যোগ করুন",
|
||||
"removeImage": "ছবি সরান",
|
||||
"uploadedImage": "আপলোড করা ছবি",
|
||||
"invalidImage": "\"{{name}}\" একটি সমর্থিত ছবি ফাইল নয়।",
|
||||
"imageTooLarge": "\"{{name}}\" {{size}} সীমা অতিক্রম করেছে।",
|
||||
"imageReadFailed": "\"{{name}}\" পড়তে ব্যর্থ হয়েছে।",
|
||||
"empty": {
|
||||
"noConfiguredModel": "কোনো মডেল কনফিগার করা নেই",
|
||||
"noConfiguredModelDescription": "চ্যাট শুরু করার আগে আপনাকে কমপক্ষে একটি AI মডেল API কী দিয়ে কনফিগার করতে হবে।",
|
||||
"goToModels": "মডেলে যান",
|
||||
"noSelectedModel": "কোনো মডেল নির্বাচিত নেই",
|
||||
"noSelectedModelDescription": "আপনি মডেল কনফিগার করেছেন, কিন্তু কোনোটি ডিফল্ট হিসেবে সেট করা নেই। চ্যাট শুরু করার আগে একটি মডেল নির্বাচন করুন।",
|
||||
"notRunning": "গেটওয়ে চলছে না",
|
||||
"notRunningDescription": "চ্যাট শুরু করতে গেটওয়ে সার্ভিস চালু করুন। উপরের বারে স্টার্ট গেটওয়ে বোতাম ব্যবহার করুন।"
|
||||
},
|
||||
"modelGroup": {
|
||||
"apikey": "API Key",
|
||||
"oauth": "OAuth",
|
||||
"local": "লোকাল"
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
"logout": {
|
||||
"tooltip": "সাইন আউট",
|
||||
"confirm": "সাইন আউট",
|
||||
"description": "আপনি কি ড্যাশবোর্ড থেকে সাইন আউট করতে চান?"
|
||||
},
|
||||
"gateway": {
|
||||
"stopDialog": {
|
||||
"title": "গেটওয়ে সার্ভিস বন্ধ করবেন?",
|
||||
"description": "আপনি কি গেটওয়ে বন্ধ করতে চান? এটি আপনার সক্রিয় চ্যাট সেশন বিচ্ছিন্ন করবে এবং ইনফারেন্স থামাবে।",
|
||||
"confirm": "গেটওয়ে বন্ধ করুন"
|
||||
},
|
||||
"action": {
|
||||
"start": "গেটওয়ে চালু করুন",
|
||||
"stop": "গেটওয়ে বন্ধ করুন",
|
||||
"restart": "গেটওয়ে পুনরায় চালু করুন"
|
||||
},
|
||||
"status": {
|
||||
"starting": "গেটওয়ে চালু হচ্ছে...",
|
||||
"restarting": "গেটওয়ে পুনরায় চালু হচ্ছে...",
|
||||
"stopping": "গেটওয়ে বন্ধ হচ্ছে..."
|
||||
},
|
||||
"restartRequired": "কনফিগারেশনের পরিবর্তন কার্যকর হতে গেটওয়ে পুনরায় চালু করা প্রয়োজন।"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"cancel": "বাতিল",
|
||||
"close": "বন্ধ করুন",
|
||||
"save": "সংরক্ষণ করুন",
|
||||
"saving": "সংরক্ষণ করা হচ্ছে...",
|
||||
"reset": "রিসেট",
|
||||
"confirm": "নিশ্চিত করুন",
|
||||
"fix": "ঠিক করুন",
|
||||
"saveChangesTitle": "আপনার অসংরক্ষিত কনফিগারেশন পরিবর্তন রয়েছে",
|
||||
"restartRequiredTitle": "গেটওয়ে পুনরায় চালু করা প্রয়োজন",
|
||||
"restartRequiredDesc": "সর্বশেষ {{name}} কনফিগারেশন সংরক্ষিত হয়েছে। কার্যকর হতে গেটওয়ে পুনরায় চালু করুন।"
|
||||
},
|
||||
"labels": {
|
||||
"loading": "লোড হচ্ছে..."
|
||||
},
|
||||
"footer": {
|
||||
"version": "সংস্করণ",
|
||||
"commit": "কমিট",
|
||||
"build": "বিল্ড",
|
||||
"version_unknown": "অজানা"
|
||||
},
|
||||
"credentials": {
|
||||
"description": "সমর্থিত প্রোভাইডারদের জন্য OAuth এবং টোকেন-ভিত্তিক ক্রেডেনশিয়াল পরিচালনা করুন।",
|
||||
"loading": "ক্রেডেনশিয়াল লোড হচ্ছে...",
|
||||
"providers": {
|
||||
"openai": {
|
||||
"description": "ব্রাউজার OAuth, ডিভাইস কোড এবং টোকেন লগইন সমর্থন করে।"
|
||||
},
|
||||
"anthropic": {
|
||||
"description": "Claude অ্যাক্সেসের জন্য টোকেন লগইন ব্যবহার করে।"
|
||||
},
|
||||
"antigravity": {
|
||||
"description": "Google Cloud Code Assist-এর জন্য ব্রাউজার OAuth ব্যবহার করে।"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"connected": "সংযুক্ত",
|
||||
"needsRefresh": "রিফ্রেশ প্রয়োজন",
|
||||
"expired": "মেয়াদ শেষ",
|
||||
"notLoggedIn": "লগ ইন করা নেই"
|
||||
},
|
||||
"actions": {
|
||||
"browser": "ব্রাউজার OAuth",
|
||||
"deviceCode": "ডিভাইস কোড",
|
||||
"stopLoading": "লোডিং বন্ধ করুন",
|
||||
"saveToken": "সংরক্ষণ করুন",
|
||||
"logout": "লগআউট"
|
||||
},
|
||||
"logoutDialog": {
|
||||
"title": "প্রোভাইডার থেকে লগআউট করবেন?",
|
||||
"description": "এটি {{provider}}-এর জন্য সংরক্ষিত আপনার ক্রেডেনশিয়াল মুছে ফেলবে।"
|
||||
},
|
||||
"fields": {
|
||||
"openaiToken": "OpenAI টোকেন",
|
||||
"anthropicToken": "Anthropic টোকেন"
|
||||
},
|
||||
"labels": {
|
||||
"account": "অ্যাকাউন্ট",
|
||||
"email": "ইমেল",
|
||||
"project": "প্রকল্প"
|
||||
},
|
||||
"errors": {
|
||||
"loadFailed": "ক্রেডেনশিয়াল লোড করতে ব্যর্থ",
|
||||
"flowFailed": "প্রমাণীকরণ প্রবাহ পরীক্ষা করতে ব্যর্থ",
|
||||
"loginFailed": "লগইন ব্যর্থ",
|
||||
"logoutFailed": "লগআউট ব্যর্থ",
|
||||
"invalidBrowserResponse": "ব্রাউজার লগইন প্রতিক্রিয়া অবৈধ",
|
||||
"invalidDeviceResponse": "ডিভাইস কোড প্রতিক্রিয়া অবৈধ",
|
||||
"popupBlocked": "নতুন ট্যাব খোলা যাচ্ছে না। অনুগ্রহ করে পপআপ অনুমতি দিন এবং আবার চেষ্টা করুন।"
|
||||
},
|
||||
"flow": {
|
||||
"current": "বর্তমান প্রমাণীকরণ অবস্থা",
|
||||
"pending": "অনুমোদনের জন্য অপেক্ষা করা হচ্ছে...",
|
||||
"success": "প্রমাণীকরণ সফল",
|
||||
"error": "প্রমাণীকরণ ব্যর্থ",
|
||||
"expired": "প্রমাণীকরণ সেশনের মেয়াদ শেষ"
|
||||
},
|
||||
"device": {
|
||||
"title": "OpenAI ডিভাইস লগইন",
|
||||
"description": "যাচাইকরণ পৃষ্ঠা খুলুন এবং নিচের কোডটি লিখুন। এই পৃষ্ঠাটি স্বয়ংক্রিয়ভাবে রিফ্রেশ হবে।",
|
||||
"code": "ইউজার কোড",
|
||||
"url": "যাচাইকরণ URL",
|
||||
"polling": "লগইন অবস্থা পোলিং করা হচ্ছে...",
|
||||
"open": "যাচাইকরণ পৃষ্ঠা খুলুন"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
"description": "AI প্রোভাইডারদের জন্য API কী কনফিগার করুন। শুধুমাত্র কনফিগার করা মডেলগুলি চ্যাটের জন্য উপলব্ধ।",
|
||||
"defaultChangeSuccess": "ডিফল্ট মডেল আপডেট করা হয়েছে।",
|
||||
"unsavedPrompt": "এই পরিবর্তন এখনও সংরক্ষিত হয়নি। মডেল কনফিগারেশনে লিখতে সংরক্ষণ করুন।",
|
||||
"restartHint": "মডেল কনফিগারেশনের পরিবর্তন গেটওয়ে পুনরায় চালু হওয়ার পরে কার্যকর হয়।",
|
||||
"loadError": "মডেল লোড করতে ব্যর্থ",
|
||||
"retry": "পুনরায় চেষ্টা করুন",
|
||||
"providerCatalogUnavailable": "ব্যাকএন্ড প্রোভাইডার ক্যাটালগ অনুপলব্ধ। মডেল API সফলভাবে লোড না হওয়া পর্যন্ত নতুন প্রোভাইডার নির্বাচন নিষ্ক্রিয়।",
|
||||
"noDefaultHintPrefix": "এখনও কোনো ডিফল্ট মডেল সেট করা নেই। সেট করতে ক্লিক করুন",
|
||||
"noDefaultHintSuffix": "এটি সেট করতে।",
|
||||
"status": {
|
||||
"available": "উপলব্ধ",
|
||||
"unconfigured": "কনফিগার করা নেই",
|
||||
"unreachable": "সার্ভিস অপ্রাপ্য"
|
||||
},
|
||||
"badge": {
|
||||
"default": "ডিফল্ট",
|
||||
"virtual": "ভার্চুয়াল"
|
||||
},
|
||||
"action": {
|
||||
"edit": "API কী সম্পাদনা করুন",
|
||||
"setDefault": "ডিফল্ট হিসেবে সেট করুন",
|
||||
"delete": "মডেল মুছুন",
|
||||
"setDefaultDisabled": {
|
||||
"setting": "ডিফল্ট হিসেবে সেট করা হচ্ছে...",
|
||||
"unavailable": "অনুপলব্ধ মডেলকে ডিফল্ট হিসেবে সেট করা যাবে না",
|
||||
"isDefault": "ইতিমধ্যে ডিফল্ট মডেল",
|
||||
"isVirtual": "ভার্চুয়াল মডেলকে ডিফল্ট হিসেবে সেট করা যাবে না",
|
||||
"unsupportedProvider": "এই প্রোভাইডারকে ডিফল্ট চ্যাট মডেল হিসেবে ব্যবহার করা যাবে না।"
|
||||
},
|
||||
"deleteDisabled": {
|
||||
"isDefault": "ডিফল্ট মডেল মুছে ফেলা যাবে না"
|
||||
}
|
||||
},
|
||||
"defaultOnSave": {
|
||||
"label": "ডিফল্ট মডেল",
|
||||
"description": "সংরক্ষণের পরে স্বয়ংক্রিয়ভাবে এই মডেলটিকে ডিফল্ট হিসেবে সেট করুন।",
|
||||
"unsupportedProvider": "এই প্রোভাইডারকে মডেল তালিকায় সংরক্ষণ করা যাবে কিন্তু ডিফল্ট চ্যাট মডেল হিসেবে ব্যবহার করা যাবে না।"
|
||||
},
|
||||
"add": {
|
||||
"button": "মডেল যোগ করুন",
|
||||
"title": "কাস্টম মডেল যোগ করুন",
|
||||
"description": "একটি OpenAI-সামঞ্জস্যপূর্ণ বা নেটিভ মডেল এন্ডপয়েন্ট যোগ করুন।",
|
||||
"modelName": "মডেল এলিয়াস",
|
||||
"modelNamePlaceholder": "যেমন my-gpt4",
|
||||
"modelNameHint": "কথোপকথনে এই মডেলটি চিহ্নিত করতে ব্যবহৃত একটি ছোট নাম।",
|
||||
"modelId": "মডেল শনাক্তকারী",
|
||||
"modelIdPlaceholder": "যেমন gpt-4o বা openai/gpt-4o",
|
||||
"modelIdHint": "এই ক্ষেত্রটি নির্বাচিত প্রোভাইডারের জন্য canonical মডেল ID হিসেবে বিবেচিত হয়। যদি শনাক্তকারীতে নিজেই একটি স্ল্যাশ থাকে (উদাহরণস্বরূপ openai/gpt-5.4), তবে এটি অপরিবর্তিত রাখা হয় এবং আবার বিভক্ত করা হয় না।",
|
||||
"errorRequired": "এই ক্ষেত্রটি প্রয়োজনীয়।",
|
||||
"errorDuplicateModelName": "মডেল এলিয়াস ইতিমধ্যে বিদ্যমান। অনুগ্রহ করে একটি ভিন্ন নাম ব্যবহার করুন।",
|
||||
"saveError": "মডেল যোগ করতে ব্যর্থ",
|
||||
"saveSuccess": "মডেল যোগ করা হয়েছে।",
|
||||
"confirm": "মডেল যোগ করুন"
|
||||
},
|
||||
"delete": {
|
||||
"title": "মডেল মুছবেন?",
|
||||
"description": "\"{{name}}\" আপনার মডেল তালিকা থেকে স্থায়ীভাবে সরানো হবে। এটি পূর্বাবস্থায় ফেরানো যাবে না।",
|
||||
"confirm": "মুছুন"
|
||||
},
|
||||
"advanced": {
|
||||
"toggle": "উন্নত বিকল্প"
|
||||
},
|
||||
"field": {
|
||||
"provider": "Provider",
|
||||
"providerPlaceholder": "একটি প্রোভাইডার নির্বাচন করুন",
|
||||
"providerHint": "ব্যাকএন্ড ক্যাটালগ থেকে একটি প্রোভাইডার নির্বাচন করুন; মডেল শনাক্তকারীকে সেই প্রোভাইডারের canonical মডেল ID হিসেবে ব্যাখ্যা করা হবে।",
|
||||
"providerInvalid": "বর্তমান প্রোভাইডার অবৈধ। অনুগ্রহ করে একটি সমর্থিত প্রোভাইডার নির্বাচন করুন।",
|
||||
"selectProviderFirst": "প্রথমে একটি প্রোভাইডার নির্বাচন করুন",
|
||||
"apiBase": "API Base URL",
|
||||
"apiKey": "API Key",
|
||||
"apiKeyPlaceholder": "আপনার API কী লিখুন",
|
||||
"apiKeyPlaceholderSet": "বিদ্যমান কী রাখতে খালি রাখুন",
|
||||
"proxy": "HTTP প্রক্সি",
|
||||
"proxyHint": "ঐচ্ছিক। যেমন http://127.0.0.1:7890",
|
||||
"authMethod": "প্রমাণীকরণ পদ্ধতি",
|
||||
"authMethodHint": "প্রমাণীকরণ পদ্ধতি: oauth, token। API কী প্রমাণীকরণের জন্য খালি রাখুন।",
|
||||
"authMethodManagedHint": "এই প্রোভাইডারের প্রমাণীকরণ পদ্ধতি সিস্টেম দ্বারা পরিচালিত।",
|
||||
"connectMode": "সংযোগ মোড",
|
||||
"connectModeHint": "CLI-ভিত্তিক প্রোভাইডারদের জন্য সংযোগ মোড: stdio বা grpc।",
|
||||
"workspace": "ওয়ার্কস্পেস পাথ",
|
||||
"workspaceHint": "CLI-ভিত্তিক প্রোভাইডারদের জন্য কাজের ডিরেক্টরি (যেমন GitHub Copilot)।",
|
||||
"requestTimeout": "অনুরোধ টাইমআউট (সেকেন্ড)",
|
||||
"requestTimeoutHint": "উত্তরের জন্য অপেক্ষা করার সর্বাধিক সেকেন্ড। 0 = ডিফল্ট ব্যবহার করুন।",
|
||||
"rpm": "রেট সীমা (RPM)",
|
||||
"rpmHint": "প্রতি মিনিটে সর্বাধিক অনুরোধ। 0 = কোনো সীমা নেই।",
|
||||
"thinkingLevel": "চিন্তার স্তর",
|
||||
"thinkingLevelHint": "thinking_level বাদ দিতে এবং প্রোভাইডার ডিফল্ট ব্যবহার করতে খালি রাখুন। মান: off, low, medium, high, xhigh, adaptive।",
|
||||
"providerDefault": "প্রোভাইডার ডিফল্ট",
|
||||
"maxTokensField": "ম্যাক্স টোকেন ফিল্ড",
|
||||
"maxTokensFieldHint": "ম্যাক্স টোকেনের জন্য অনুরোধ ক্ষেত্রের নাম ওভাররাইড করুন, যেমন max_completion_tokens।",
|
||||
"toolSchemaTransform": "টুল স্কিমা ট্রান্সফর্ম",
|
||||
"toolSchemaTransformHint": "টুল JSON স্কিমার জন্য ঐচ্ছিক সামঞ্জস্যতা ট্রান্সফর্ম। নেটিভ আচরণের জন্য খালি রাখুন। সমর্থিত মান: simple।",
|
||||
"streamingEnabled": "স্ট্রিমিং আউটপুট",
|
||||
"streamingEnabledHint": "এই মডেল এন্ট্রিকে প্রোভাইডার স্ট্রিমিং অনুরোধ চেষ্টা করার অনুমতি দিন। বর্তমান চ্যানেল স্ট্রিমিং সুইচও সক্ষম থাকতে হবে।",
|
||||
"extraBody": "Extra Body",
|
||||
"extraBodyHint": "অনুরোধ বডিতে ইনজেক্ট করার অতিরিক্ত JSON ক্ষেত্র, যেমন {\"reasoning_split\": true}।",
|
||||
"customHeaders": "Custom Headers",
|
||||
"customHeadersHint": "প্রতিটি অনুরোধে ইনজেক্ট করার অতিরিক্ত HTTP হেডার, যেমন {\"X-Source\": \"coding-plan\"}।",
|
||||
"invalidJson": "অবৈধ JSON ফর্ম্যাট"
|
||||
},
|
||||
"edit": {
|
||||
"title": "{{name}} কনফিগার করুন",
|
||||
"apiKeyHint": "একটি কী ইতিমধ্যে সেট করা হয়েছে। অপরিবর্তিত রাখতে খালি রাখুন।",
|
||||
"oauthNote": "এই প্রোভাইডার OAuth ব্যবহার করে — কোনো API কী প্রয়োজন নেই।",
|
||||
"saveError": "সংরক্ষণ করতে ব্যর্থ",
|
||||
"saveSuccess": "মডেল কনফিগারেশন সংরক্ষিত হয়েছে।"
|
||||
},
|
||||
"fetch": {
|
||||
"title": "উপলব্ধ মডেল আনুন",
|
||||
"description": "আপস্ট্রিম প্রোভাইডার থেকে মডেল তালিকা আনুন।",
|
||||
"providerLabel": "প্রোভাইডার:",
|
||||
"needApiKey": "মডেল আনতে অনুগ্রহ করে প্রথমে একটি API কী লিখুন।",
|
||||
"fetching": "মডেল আনা হচ্ছে...",
|
||||
"retry": "পুনরায় চেষ্টা করুন",
|
||||
"filterPlaceholder": "মডেল ফিল্টার করুন...",
|
||||
"found": "{{count}}টি মডেল পাওয়া গেছে",
|
||||
"found_plural": "{{count}}টি মডেল পাওয়া গেছে",
|
||||
"shown": "({{count}}টি দেখানো হয়েছে)",
|
||||
"selectAll": "সব নির্বাচন করুন",
|
||||
"deselectAll": "সব নির্বাচন বাতিল করুন",
|
||||
"fill": "{{count}}টি নির্বাচিত মডেল পূরণ করুন",
|
||||
"fill_plural": "{{count}}টি নির্বাচিত মডেল পূরণ করুন",
|
||||
"failed": "মডেল আনতে ব্যর্থ"
|
||||
},
|
||||
"catalog": {
|
||||
"button": "সংরক্ষিত ক্যাটালগ",
|
||||
"title": "সংরক্ষিত মডেল ক্যাটালগ",
|
||||
"description": "পূর্বে আনা মডেল তালিকা, প্রতি API কী অনুযায়ী সংরক্ষিত। আপনার কনফিগারেশনে যোগ করতে মডেল নির্বাচন করুন।",
|
||||
"loading": "ক্যাটালগ লোড হচ্ছে...",
|
||||
"empty": "এখনও কোনো সংরক্ষিত ক্যাটালগ নেই। একটি ক্যাটালগ সংরক্ষণ করতে প্রোভাইডার থেকে মডেল আনুন।",
|
||||
"filterPlaceholder": "মডেল ফিল্টার করুন...",
|
||||
"models": "মডেল",
|
||||
"fetchedAt": "আনা হয়েছে",
|
||||
"delete": "ক্যাটালগ মুছুন",
|
||||
"refresh": "আপস্ট্রিম থেকে রিফ্রেশ করুন",
|
||||
"found": "{{count}}টি মডেল পাওয়া গেছে",
|
||||
"found_plural": "{{count}}টি মডেল পাওয়া গেছে",
|
||||
"selectAll": "সব নির্বাচন করুন",
|
||||
"deselectAll": "সব নির্বাচন বাতিল করুন",
|
||||
"addSelected": "{{count}}টি নির্বাচিত যোগ করুন",
|
||||
"addSuccess": "কনফিগারেশনে {{count}}টি মডেল যোগ করা হয়েছে।",
|
||||
"needApiKey": "এই মডেলগুলির একটি API কী প্রয়োজন। আমদানির পরে আপনাকে ক্রেডেনশিয়াল কনফিগার করতে হবে।"
|
||||
},
|
||||
"test": {
|
||||
"title": "মডেল সংযোগ পরীক্ষা করুন",
|
||||
"description": "যাচাই করুন যে মডেল এন্ডপয়েন্টটি অ্যাক্সেসযোগ্য এবং সঠিকভাবে কনফিগার করা হয়েছে।",
|
||||
"modelLabel": "মডেল:",
|
||||
"identifierLabel": "শনাক্তকারী:",
|
||||
"endpointLabel": "এন্ডপয়েন্ট:",
|
||||
"testConnection": "সংযোগ পরীক্ষা করুন",
|
||||
"testing": "সংযোগ পরীক্ষা করা হচ্ছে...",
|
||||
"success": "সংযোগ সফল",
|
||||
"responseTime": "প্রতিক্রিয়া সময়: {{ms}}ms",
|
||||
"failed": "সংযোগ ব্যর্থ",
|
||||
"status": "স্ট্যাটাস: {{status}}",
|
||||
"testFailed": "পরীক্ষা ব্যর্থ",
|
||||
"testAgain": "আবার পরীক্ষা করুন"
|
||||
},
|
||||
"validation": {
|
||||
"whitespace": "মডেল শনাক্তকারীতে স্পেস থাকতে পারবে না",
|
||||
"leadingSlash": "/ দিয়ে শুরু হওয়া উচিত নয়",
|
||||
"consecutiveSlash": "পরপর / থাকা উচিত নয়",
|
||||
"useProvider": "প্রোভাইডার হিসেবে \"{{provider}}\" ব্যবহার করা হবে",
|
||||
"defaultToOpenAI": "কোনো প্রোভাইডার নির্দিষ্ট করা নেই, ডিফল্ট OpenAI",
|
||||
"emptyModel": "মডেলের নাম খালি হতে পারবে না",
|
||||
"shouldUse": "\"{{provider}}\" এর \"{{alias}}\" ব্যবহার করা উচিত",
|
||||
"didYouMean": "আপনি কি \"{{closest}}\" বোঝাতে চেয়েছেন?",
|
||||
"unknownProvider": "অজানা প্রোভাইডার \"{{provider}}\"",
|
||||
"parsed": "প্রোভাইডার={{provider}}, মডেল={{model}}"
|
||||
},
|
||||
"combobox": {
|
||||
"selectProvider": "প্রোভাইডার নির্বাচন করুন...",
|
||||
"searchProvider": "প্রোভাইডার অনুসন্ধান করুন...",
|
||||
"noProvider": "কোনো প্রোভাইডার পাওয়া যায়নি।",
|
||||
"noCatalog": "প্রোভাইডার ক্যাটালগ অনুপলব্ধ।",
|
||||
"local": "লোকাল"
|
||||
}
|
||||
},
|
||||
"channels": {
|
||||
"loadError": "চ্যানেল লোড করতে ব্যর্থ",
|
||||
"name": {
|
||||
"telegram": "Telegram",
|
||||
"discord": "Discord",
|
||||
"slack": "Slack",
|
||||
"feishu": "ফেইশু",
|
||||
"dingtalk": "ডিংটক",
|
||||
"line": "LINE",
|
||||
"qq": "QQ",
|
||||
"onebot": "OneBot",
|
||||
"wecom": "উইকম",
|
||||
"whatsapp": "WhatsApp",
|
||||
"whatsapp_native": "WhatsApp Native",
|
||||
"pico": "Web",
|
||||
"maixcam": "MaixCam",
|
||||
"matrix": "Matrix",
|
||||
"irc": "IRC",
|
||||
"weixin": "উইচ্যাট",
|
||||
"mqtt": "MQTT"
|
||||
},
|
||||
"weixin": {
|
||||
"bindTitle": "WeChat অ্যাকাউন্ট বাইন্ডিং",
|
||||
"bindDesc": "আপনার ব্যক্তিগত অ্যাকাউন্ট বাইন্ড করতে WeChat দিয়ে QR কোড স্ক্যান করুন।",
|
||||
"bind": "WeChat বাইন্ড করুন",
|
||||
"rebind": "পুনরায় বাইন্ড করুন",
|
||||
"bound": "WeChat বাইন্ড করা হয়েছে",
|
||||
"notBound": "WeChat অ্যাকাউন্ট এখনও বাইন্ড করা হয়নি।",
|
||||
"generating": "QR কোড তৈরি করা হচ্ছে...",
|
||||
"scanHint": "WeChat খুলুন এবং QR কোড স্ক্যান করুন",
|
||||
"scanned": "স্ক্যান করা হয়েছে — অনুগ্রহ করে WeChat-এ নিশ্চিত করুন",
|
||||
"expired": "QR কোডের মেয়াদ শেষ",
|
||||
"retry": "আবার চেষ্টা করুন",
|
||||
"refresh": "QR রিফ্রেশ করুন",
|
||||
"errorGeneric": "একটি ত্রুটি ঘটেছে। অনুগ্রহ করে আবার চেষ্টা করুন।"
|
||||
},
|
||||
"wecom": {
|
||||
"bindTitle": "WeCom বাইন্ডিং",
|
||||
"bindDesc": "আপনার AI বট বাইন্ড করতে WeCom দিয়ে QR কোড স্ক্যান করুন।",
|
||||
"bind": "WeCom বাইন্ড করুন",
|
||||
"rebind": "পুনরায় বাইন্ড করুন",
|
||||
"bound": "WeCom বাইন্ড করা হয়েছে",
|
||||
"notBound": "WeCom AI বট এখনও বাইন্ড করা হয়নি।",
|
||||
"generating": "QR কোড তৈরি করা হচ্ছে...",
|
||||
"scanHint": "WeCom খুলুন এবং QR কোড স্ক্যান করুন",
|
||||
"scanned": "স্ক্যান করা হয়েছে, অনুগ্রহ করে WeCom-এ নিশ্চিত করুন",
|
||||
"expired": "QR কোডের মেয়াদ শেষ",
|
||||
"retry": "আবার চেষ্টা করুন",
|
||||
"refresh": "QR রিফ্রেশ করুন",
|
||||
"errorGeneric": "একটি ত্রুটি ঘটেছে। অনুগ্রহ করে আবার চেষ্টা করুন।"
|
||||
},
|
||||
"field": {
|
||||
"token": "Bot Token",
|
||||
"tokenPlaceholder": "বট টোকেন লিখুন",
|
||||
"botToken": "Bot Token",
|
||||
"appToken": "App Token",
|
||||
"appId": "App ID",
|
||||
"appSecret": "App Secret",
|
||||
"verificationToken": "Verification Token",
|
||||
"encryptKey": "Encrypt Key",
|
||||
"baseUrl": "API Base URL",
|
||||
"proxy": "HTTP প্রক্সি",
|
||||
"mentionOnly": "শুধুমাত্র উল্লেখ করলে",
|
||||
"typingEnabled": "টাইপিং সূচক",
|
||||
"placeholderEnabled": "প্লেসহোল্ডার বার্তা",
|
||||
"placeholderText": "প্লেসহোল্ডার টেক্সট",
|
||||
"streamingEnabled": "স্ট্রিমিং আউটপুট",
|
||||
"streamingThrottleSeconds": "আপডেট ব্যবধান (সেকেন্ড)",
|
||||
"streamingMinGrowthChars": "ন্যূনতম বৃদ্ধির অক্ষর",
|
||||
"groupTriggerMentionOnly": "গ্রুপে শুধুমাত্র উল্লেখ",
|
||||
"groupTriggerPrefixes": "গ্রুপ ট্রিগার প্রিফিক্স",
|
||||
"groupTriggerPrefixesPlaceholder": "যেমন /, !, ?",
|
||||
"randomReactionEmoji": "র্যান্ডম প্রতিক্রিয়া ইমোজি",
|
||||
"randomReactionEmojiPlaceholder": "যেমন THUMBSUP, HEART, SMILE",
|
||||
"isLark": "Lark (আন্তর্জাতিক)",
|
||||
"allowFrom": "যাদের থেকে অনুমতি",
|
||||
"allowFromPlaceholder": "যেমন 123456, 789012",
|
||||
"allowOrigins": "অনুমোদিত অরিজিন",
|
||||
"allowOriginsPlaceholder": "যেমন https://example.com, http://localhost:5173",
|
||||
"removeListItem": "{{value}} সরান",
|
||||
"secretPlaceholder": "সিক্রেট লিখুন",
|
||||
"secretHintSet": "একটি মান ইতিমধ্যে সেট করা হয়েছে। অপরিবর্তিত রাখতে খালি রাখুন।"
|
||||
},
|
||||
"page": {
|
||||
"notFound": "চ্যানেল \"{{name}}\" সমর্থিত নয়।",
|
||||
"saveSuccess": "চ্যানেল কনফিগারেশন সংরক্ষিত হয়েছে।",
|
||||
"saveError": "চ্যানেল কনফিগারেশন সংরক্ষণ করতে ব্যর্থ",
|
||||
"savePrompt": "এই পরিবর্তন এখনও সংরক্ষিত হয়নি। চ্যানেল কনফিগারেশনে লিখতে সংরক্ষণ করুন।",
|
||||
"docLink": "ডকুমেন্টেশন",
|
||||
"enableLabel": "চ্যানেল সক্ষম করুন",
|
||||
"restartRequiredTitle": "গেটওয়ে পুনরায় চালু করা প্রয়োজন",
|
||||
"restartRequiredDesc": "সর্বশেষ {{name}} কনফিগারেশন সংরক্ষিত হয়েছে। কার্যকর হতে গেটওয়ে পুনরায় চালু করুন।"
|
||||
},
|
||||
"form": {
|
||||
"desc": {
|
||||
"token": "প্ল্যাটফর্ম API-এর সাথে সংযোগের জন্য ব্যবহৃত বট অ্যাক্সেস টোকেন।",
|
||||
"botToken": "বার্তা পাঠাতে এবং গ্রহণ করতে ব্যবহৃত বট টোকেন।",
|
||||
"appToken": "Socket Mode সংযোগের জন্য ব্যবহৃত অ্যাপ টোকেন।",
|
||||
"appId": "প্রমাণীকরণের জন্য ব্যবহৃত অনন্য অ্যাপ্লিকেশন ID।",
|
||||
"appSecret": "স্বাক্ষর এবং প্রমাণীকরণের জন্য ব্যবহৃত অ্যাপ্লিকেশন সিক্রেট।",
|
||||
"verificationToken": "ইভেন্ট কলব্যাকের জন্য যাচাইকরণ টোকেন।",
|
||||
"encryptKey": "কলব্যাক পেলোড ডিক্রিপ্ট করতে ব্যবহৃত এনক্রিপশন কী।",
|
||||
"baseUrl": "প্ল্যাটফর্ম API বেস URL। ডিফল্টরূপে অফিসিয়াল এন্ডপয়েন্ট ব্যবহৃত হয়।",
|
||||
"proxy": "বহির্গামী নেটওয়ার্ক অ্যাক্সেসের জন্য HTTP প্রক্সি ঠিকানা।",
|
||||
"mentionOnly": "গ্রুপ চ্যাটে শুধুমাত্র বটকে স্পষ্টভাবে উল্লেখ করলে প্রতিক্রিয়া জানান।",
|
||||
"typingEnabled": "সহকারী প্রতিক্রিয়া তৈরি করার সময় টাইপিং স্ট্যাটাস প্রদর্শন করুন।",
|
||||
"placeholderEnabled": "চূড়ান্ত উত্তর পাঠানোর আগে অস্থায়ী প্লেসহোল্ডার বার্তা সক্ষম করুন।",
|
||||
"streamingEnabled": "এই চ্যানেলকে প্রোভাইডার স্ট্রিমিং আউটপুট প্রদর্শনের অনুমতি দিন। বর্তমান মডেল এন্ট্রি স্ট্রিমিং সুইচও সক্ষম থাকতে হবে।",
|
||||
"streamingThrottleSeconds": "মধ্যবর্তী স্ট্রিমিং আপডেটগুলির মধ্যে ন্যূনতম ব্যবধান। 0 মানে ডিফল্ট ব্যবহার করুন। চূড়ান্ত উত্তরে থ্রটল করা হয় না।",
|
||||
"streamingMinGrowthChars": "অন্য একটি মধ্যবর্তী স্ট্রিমিং আপডেট পাঠানোর আগে ন্যূনতম টেক্সট বৃদ্ধি। 0 মানে ডিফল্ট ব্যবহার করুন। চূড়ান্ত উত্তরে থ্রটল করা হয় না।",
|
||||
"groupTriggerMentionOnly": "গ্রুপ চ্যাটে, শুধুমাত্র বটকে উল্লেখ করলেই প্রতিক্রিয়া জানান।",
|
||||
"groupTriggerPrefixes": "কাস্টম গ্রুপ-চ্যাট ট্রিগার প্রিফিক্স। একে একে আইটেম যোগ করুন, অথবা একাধিক মান একবারে পেস্ট করুন।",
|
||||
"randomReactionEmoji": "PicoClaw প্রাপ্তি নিশ্চিত করতে ব্যবহারকারীর বার্তায় ইমোজি প্রতিক্রিয়া যোগ করে। উদাহরণ: \"THUMBSUP\", \"HEART\", \"SMILE\"। ডিফল্ট \"Pin\" ইমোজি ব্যবহার করতে খালি রাখুন।",
|
||||
"isLark": "Feishu ডোমেইন (open.feishu.cn) এর পরিবর্তে Lark আন্তর্জাতিক ডোমেইন (open.larksuite.com) ব্যবহার করুন।",
|
||||
"allowFrom": "অনুমোদিত ব্যবহারকারী বা গ্রুপ ID। একে একে আইটেম যোগ করুন, অথবা একাধিক মান একবারে পেস্ট করুন।",
|
||||
"allowOrigins": "অনুমোদিত অরিজিন ডোমেইন। একে একে আইটেম যোগ করুন, অথবা একাধিক মান একবারে পেস্ট করুন।",
|
||||
"wsUrl": "WebSocket সার্ভিস URL।",
|
||||
"reconnectInterval": "বিচ্ছিন্ন হওয়ার পরে পুনঃসংযোগের ব্যবধান (সেকেন্ড)।",
|
||||
"bridgeUrl": "ব্রিজ সার্ভিস URL।",
|
||||
"sessionStorePath": "সেশন স্টোরেজের জন্য লোকাল পাথ।",
|
||||
"useNative": "নেটিভ ক্লায়েন্ট মোড ব্যবহার করবেন কিনা।",
|
||||
"host": "সার্ভিস হোস্ট ঠিকানা।",
|
||||
"port": "সার্ভিস পোর্ট।",
|
||||
"homeserver": "Matrix হোমসার্ভার URL।",
|
||||
"userId": "অ্যাকাউন্ট ইউজার ID।",
|
||||
"deviceId": "ডিভাইস ID।",
|
||||
"joinOnInvite": "আমন্ত্রিত হলে স্বয়ংক্রিয়ভাবে রুমে যোগ দিন।",
|
||||
"clientId": "প্ল্যাটফর্ম প্রমাণীকরণের জন্য ব্যবহৃত ক্লায়েন্ট ID।",
|
||||
"corpId": "এন্টারপ্রাইজ Corp ID।",
|
||||
"agentId": "এন্টারপ্রাইজ অ্যাপ্লিকেশন এজেন্ট ID।",
|
||||
"webhookUrl": "সম্পূর্ণ webhook URL।",
|
||||
"webhookHost": "Webhook শোনার হোস্ট।",
|
||||
"webhookPort": "Webhook শোনার পোর্ট।",
|
||||
"webhookPath": "Webhook রুট পাথ।",
|
||||
"replyTimeout": "সেকেন্ডে উত্তরের টাইমআউট।",
|
||||
"maxSteps": "প্রক্রিয়াকরণ ধাপের সর্বাধিক সংখ্যা।",
|
||||
"welcomeMessage": "নতুন সেশনের জন্য স্বাগত বার্তার বিষয়বস্তু।",
|
||||
"allowTokenQuery": "URL কোয়েরি প্যারামিটারে টোকেন অনুমতি দিন।",
|
||||
"pingInterval": "সেকেন্ডে সংযোগ হার্টবিট ব্যবধান।",
|
||||
"readTimeout": "সেকেন্ডে রিড টাইমআউট।",
|
||||
"writeTimeout": "সেকেন্ডে রাইট টাইমআউট।",
|
||||
"maxConnections": "একযোগে সংযোগের সর্বাধিক সংখ্যা।",
|
||||
"server": "IRC সার্ভার ঠিকানা।",
|
||||
"tls": "TLS সক্ষম করবেন কিনা।",
|
||||
"nick": "বট ডাকনাম।",
|
||||
"user": "IRC ব্যবহারকারীর নাম।",
|
||||
"realName": "প্রদর্শিত আসল নাম।",
|
||||
"channels": "যোগ দিতে IRC চ্যানেল।",
|
||||
"requestCaps": "সংযোগে অনুরোধ করা IRC ক্ষমতার তালিকা।",
|
||||
"maxBase64FileSizeMiB": "আপলোডের আগে লোকাল ফাইল base64-এ রূপান্তরের জন্য MiB-এ সর্বাধিক আকার। 0 মানে সীমাহীন। শুধুমাত্র লোকাল ফাইলের জন্য প্রযোজ্য, URL আপলোডের জন্য নয়।",
|
||||
"genericField": "{{field}} কনফিগার করতে ব্যবহৃত।",
|
||||
"broker": "MQTT ব্রোকার ঠিকানা।",
|
||||
"mqttAgentId": "এই ইনস্ট্যান্সের জন্য অনন্য শনাক্তকারী, টপিক পাথ তৈরিতে ব্যবহৃত।",
|
||||
"topicPrefix": "টপিক প্রিফিক্স। ডিফল্ট /picoclaw।",
|
||||
"mqttUsername": "ব্রোকার প্রমাণীকরণ ব্যবহারকারীর নাম (ঐচ্ছিক)।",
|
||||
"mqttPassword": "ব্রোকার প্রমাণীকরণ পাসওয়ার্ড (ঐচ্ছিক)।",
|
||||
"mqttClientId": "MQTT ক্লায়েন্ট ID। স্বয়ংক্রিয়ভাবে তৈরি করতে খালি রাখুন।",
|
||||
"keepAlive": "সেকেন্ডে Keepalive ব্যবধান। ডিফল্ট 60।",
|
||||
"qos": "বার্তার সেবার গুণমান স্তর: 0 = সর্বাধিক একবার, 1 = কমপক্ষে একবার, 2 = ঠিক একবার।"
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"requiredField": "এই ক্ষেত্রটি প্রয়োজনীয়।"
|
||||
},
|
||||
"mqtt": {
|
||||
"protocolTitle": "প্রোটোকল রেফারেন্স",
|
||||
"protocolDesc": "ক্লায়েন্টরা নিম্নলিখিত টপিক এবং পেলোড ফর্ম্যাট ব্যবহার করে বার্তা পাঠায় এবং গ্রহণ করে।",
|
||||
"uplink": "আপলিঙ্ক (ক্লায়েন্ট → এজেন্ট)",
|
||||
"downlink": "ডাউনলিঙ্ক (এজেন্ট → ক্লায়েন্ট)",
|
||||
"topicParams": "টপিক প্যারামিটার",
|
||||
"fieldText": "text",
|
||||
"uplinkTextDesc": "ব্যবহারকারীর কাছ থেকে প্রাকৃতিক ভাষার নির্দেশনা (প্রয়োজনীয়)।",
|
||||
"downlinkTextDesc": "এজেন্টের উত্তর টেক্সট। স্ট্রিমিং মোডে, সম্পূর্ণ প্রতিক্রিয়ার জন্য একাধিক বার্তা ক্রমানুসারে যুক্ত করুন।",
|
||||
"topicPrefixDesc": "টপিক প্রিফিক্স, উপরের কনফিগারেশনের সাথে মেলে।",
|
||||
"agentIdDesc": "এজেন্ট ID, উপরের কনফিগারেশনের সাথে মেলে।",
|
||||
"clientIdDesc": "ক্লায়েন্ট-সংজ্ঞায়িত শনাক্তকারী। সুপারিশ: প্রথম চালু হওয়ার সময় একটি UUID তৈরি করুন এবং এটি সংরক্ষণ করুন যাতে একই ডিভাইস সবসময় একই ID ব্যবহার করে।",
|
||||
"clientIdPlaceholder": "খালি থাকলে স্বয়ংক্রিয়ভাবে তৈরি",
|
||||
"secretSet": "ইতিমধ্যে কনফিগার করা হয়েছে। অপরিবর্তিত রাখতে খালি রাখুন।",
|
||||
"secretEmpty": "কনফিগার করা নেই"
|
||||
}
|
||||
},
|
||||
"pages": {
|
||||
"agent": {
|
||||
"load_error": "এজেন্ট সমর্থন তথ্য লোড করতে ব্যর্থ।",
|
||||
"skills": {
|
||||
"empty": "বর্তমানে কোনো দক্ষতা উপলব্ধ নেই।",
|
||||
"install_success": "{{name}} ইনস্টল করা হয়েছে।",
|
||||
"install_error": "দক্ষতা ইনস্টল করতে ব্যর্থ।",
|
||||
"search_placeholder": "নাম, বিবরণ বা রেজিস্ট্রি দ্বারা অনুসন্ধান করুন",
|
||||
"source_label": "প্রকার",
|
||||
"sort_label": "সাজান",
|
||||
"import": "দক্ষতা আমদানি করুন",
|
||||
"import_success": "দক্ষতা আমদানি করা হয়েছে।",
|
||||
"import_error": "দক্ষতা আমদানি করতে ব্যর্থ।",
|
||||
"import_invalid_type": "শুধুমাত্র Markdown বা ZIP দক্ষতা ফাইল সমর্থিত।",
|
||||
"import_invalid_size": "দক্ষতা ফাইল 1 MB বা ছোট হতে হবে।",
|
||||
"import_constraints": "1 MB পর্যন্ত একটি Markdown বা ZIP দক্ষতা ফাইল আমদানি করুন",
|
||||
"view": "দেখুন",
|
||||
"delete": "মুছুন",
|
||||
"delete_title": "দক্ষতা মুছবেন?",
|
||||
"delete_description": "\"{{name}}\" ওয়ার্কস্পেস দক্ষতা থেকে সরানো হবে।",
|
||||
"delete_confirm": "মুছুন",
|
||||
"delete_success": "দক্ষতা মুছে ফেলা হয়েছে।",
|
||||
"delete_error": "দক্ষতা মুছতে ব্যর্থ।",
|
||||
"viewer_title": "দক্ষতার বিষয়বস্তু",
|
||||
"viewer_description": "এখানে বর্তমানে কার্যকর SKILL.md বিষয়বস্তু পড়ুন।",
|
||||
"load_detail_error": "দক্ষতার বিষয়বস্তু লোড করতে ব্যর্থ।",
|
||||
"no_description": "কোনো বিবরণ প্রদান করা হয়নি।",
|
||||
"no_results": "বর্তমান ফিল্টারের সাথে কোনো দক্ষতা মেলেনি।",
|
||||
"dropzone_title": "ওয়ার্কস্পেসে আমদানি করুন",
|
||||
"dropzone_description": "এখানে একটি দক্ষতা ফাইল টেনে আনুন বা ডিস্ক থেকে নির্বাচন করুন।",
|
||||
"dropzone_label": "এখানে একটি দক্ষতা ফাইল ফেলুন",
|
||||
"dropzone_active": "এই দক্ষতাটি আমদানি করতে ছাড়ুন",
|
||||
"dropzone_release": "দক্ষতাটি স্বাভাবিকীকরণ করা হবে এবং ওয়ার্কস্পেস দক্ষতা ডিরেক্টরিতে সংরক্ষণ করা হবে।",
|
||||
"marketplace_title": "দক্ষতা আবিষ্কার করুন",
|
||||
"marketplace_description": "দক্ষতা রেজিস্ট্রি অনুসন্ধান করুন এবং এই ওয়ার্কস্পেসে দরকারী দক্ষতা ইনস্টল করুন",
|
||||
"marketplace_search_placeholder": "github, docker, database এর মতো ক্ষমতার জন্য অনুসন্ধান করুন...",
|
||||
"marketplace_search_action": "অনুসন্ধান",
|
||||
"marketplace_search_status": "অনুসন্ধান স্ট্যাটাস",
|
||||
"marketplace_install_status": "ইনস্টল স্ট্যাটাস",
|
||||
"marketplace_notice_title": "নিরাপত্তা বিজ্ঞপ্তি",
|
||||
"marketplace_notice_body": "রেজিস্ট্রি দক্ষতা তৃতীয় পক্ষের বিষয়বস্তু। ইনস্টল করার আগে লেখক, পৃষ্ঠার URL, নির্দেশাবলী এবং প্রয়োজনীয় কোড বা ক্রেডেনশিয়াল পর্যালোচনা করুন।",
|
||||
"marketplace_status_disabled": "নিষ্ক্রিয়। প্রথমে টুল পৃষ্ঠায় সংশ্লিষ্ট টুল সক্ষম করুন।",
|
||||
"marketplace_status_enable_hint": "প্রথমে টুল পৃষ্ঠায় সম্পর্কিত টুল সক্ষম করুন।",
|
||||
"marketplace_search_error": "রেজিস্ট্রি অনুসন্ধান করতে ব্যর্থ।",
|
||||
"marketplace_loading_results": "দক্ষতা অনুসন্ধান করা হচ্ছে...",
|
||||
"marketplace_loading_more": "আরও দক্ষতা লোড করা হচ্ছে...",
|
||||
"marketplace_results_title": "“{{query}}”-এর জন্য {{count}}টি ফলাফল",
|
||||
"marketplace_results_hint": "রেজিস্ট্রি ফলাফল বর্তমান ওয়ার্কস্পেসে ইনস্টল হয়।",
|
||||
"marketplace_install_action": "ইনস্টল",
|
||||
"marketplace_installed": "ইনস্টল করা হয়েছে",
|
||||
"marketplace_view_installed": "লোকাল দেখুন",
|
||||
"marketplace_installed_hint": "“{{name}}” হিসেবে এই ওয়ার্কস্পেসে ইতিমধ্যে উপলব্ধ।",
|
||||
"marketplace_empty_results": "“{{query}}”-এর সাথে কোনো ইনস্টলযোগ্য দক্ষতা মেলেনি।",
|
||||
"marketplace_idle": "কনফিগার করা রেজিস্ট্রি থেকে ইনস্টলযোগ্য দক্ষতা আবিষ্কার করতে একটি ক্ষমতার জন্য অনুসন্ধান করুন।",
|
||||
"marketplace_unavailable": "রেজিস্ট্রি অনুসন্ধান বর্তমানে অনুপলব্ধ। দক্ষতা টুল কনফিগারেশন পরীক্ষা করুন।",
|
||||
"sort": {
|
||||
"name_asc": "নাম (A-Z)",
|
||||
"name_desc": "নাম (Z-A)",
|
||||
"source": "প্রকার"
|
||||
},
|
||||
"origin": {
|
||||
"all": "সব প্রকার",
|
||||
"builtin": "বিল্টইন",
|
||||
"third_party": "তৃতীয় পক্ষ",
|
||||
"manual": "ম্যানুয়াল"
|
||||
},
|
||||
"summary": {
|
||||
"total": "মোট দক্ষতা"
|
||||
},
|
||||
"detail_tabs": {
|
||||
"preview": "প্রিভিউ",
|
||||
"raw": "র",
|
||||
"meta": "মেটাডেটা"
|
||||
},
|
||||
"metadata": {
|
||||
"name": "নাম",
|
||||
"description": "বিবরণ",
|
||||
"registry": "রেজিস্ট্রি",
|
||||
"url": "লিঙ্ক ঠিকানা",
|
||||
"version": "ইনস্টল করা সংস্করণ",
|
||||
"lines": "লাইন সংখ্যা",
|
||||
"characters": "অক্ষর সংখ্যা"
|
||||
},
|
||||
"marketplace_installDisabled": {
|
||||
"installing": "ইনস্টল করা হচ্ছে...",
|
||||
"installed": "ইতিমধ্যে ইনস্টল করা হয়েছে",
|
||||
"cannotInstall": "ইনস্টল করা যাবে না: সম্পর্কিত টুল সক্ষম নয়"
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"search_placeholder": "টুল অনুসন্ধান করুন...",
|
||||
"no_results": "আপনার মানদণ্ডের সাথে কোনো টুল মেলেনি।",
|
||||
"filter": {
|
||||
"all": "সব স্ট্যাটাস",
|
||||
"enabled": "সক্ষম",
|
||||
"disabled": "নিষ্ক্রিয়",
|
||||
"blocked": "ব্লক করা"
|
||||
},
|
||||
"empty": "কোনো টুল উপলব্ধ নেই।",
|
||||
"enable_success": "টুল সক্ষম করা হয়েছে।",
|
||||
"disable_success": "টুল নিষ্ক্রিয় করা হয়েছে।",
|
||||
"toggle_error": "টুলের অবস্থা আপডেট করতে ব্যর্থ।",
|
||||
"library_title": "টুল লাইব্রেরি",
|
||||
"library_description": "আপনার AI এজেন্টদের জন্য উপলব্ধ টুলসেট ব্রাউজ এবং পরিচালনা করুন।",
|
||||
"web_search": {
|
||||
"title": "ওয়েব অনুসন্ধান",
|
||||
"description": "এজেন্টদের সর্বশেষ বাস্তব-বিশ্বের তথ্য খুঁজে পেতে ওয়েব অনুসন্ধান ক্ষমতা প্রদান করুন। স্বয়ংক্রিয়ভাবে সর্বোত্তম সক্রিয় প্রোভাইডারে রুট করে।",
|
||||
"unsaved_prompt": "এই পরিবর্তন এখনও সংরক্ষিত হয়নি। ওয়েব অনুসন্ধান কনফিগারেশনে লিখতে সংরক্ষণ করুন।",
|
||||
"global_settings": "সাধারণ",
|
||||
"providers_config": "ইন্টিগ্রেশন",
|
||||
"load_error": "ওয়েব অনুসন্ধান কনফিগারেশন লোড করতে ব্যর্থ।",
|
||||
"save": "পরিবর্তন সংরক্ষণ করুন",
|
||||
"open_settings": "সেটিংস খুলুন",
|
||||
"save_success": "সেটিংস সফলভাবে সংরক্ষিত হয়েছে।",
|
||||
"save_error": "সেটিংস সংরক্ষণ করতে ব্যর্থ।",
|
||||
"provider": "প্রাথমিক প্রোভাইডার",
|
||||
"provider_description": "ওয়েব অনুসন্ধান টুল একটি অনুরোধ পরিচালনা করার সময় ব্যবহার করার জন্য ডিফল্ট প্রোভাইডার নির্বাচন করুন।",
|
||||
"proxy": "HTTPS প্রক্সি",
|
||||
"proxy_description": "অন্তর্নিহিত ওয়েব অনুরোধের জন্য ঐচ্ছিক বৈশ্বিক HTTP/S প্রক্সি।",
|
||||
"prefer_native": "নেটিভ অনুসন্ধান পছন্দ করুন",
|
||||
"prefer_native_hint": "সক্ষম থাকলে, মডেলটি কনফিগার করা প্রোভাইডার তালিকার পরিবর্তে তার বিল্ট-ইন অনুসন্ধান ক্ষমতা ব্যবহার করতে পারে।",
|
||||
"provider_hint": "এই প্রোভাইডারকে সক্ষম করুন এবং কোনো প্রয়োজনীয় সংযোগ সেটিংস পূরণ করুন।",
|
||||
"max_results": "সর্বাধিক ফলাফল",
|
||||
"base_url": "বেস URL",
|
||||
"base_url_placeholder": "ঐচ্ছিক এন্ডপয়েন্ট ওভাররাইড",
|
||||
"api_key": "API কী / টোকেন",
|
||||
"api_key_placeholder": "API কী লিখুন, আসল কী রাখতে এটি খালি রাখুন",
|
||||
"none": "অনুপলব্ধ"
|
||||
},
|
||||
"status": {
|
||||
"enabled": "সক্ষম",
|
||||
"disabled": "নিষ্ক্রিয়",
|
||||
"blocked": "ব্লক করা"
|
||||
},
|
||||
"categories": {
|
||||
"automation": "অটোমেশন",
|
||||
"filesystem": "ফাইলসিস্টেম",
|
||||
"web": "ওয়েব",
|
||||
"communication": "যোগাযোগ",
|
||||
"skills": "দক্ষতা",
|
||||
"agents": "এজেন্ট",
|
||||
"hardware": "হার্ডওয়্যার",
|
||||
"discovery": "আবিষ্কার"
|
||||
},
|
||||
"reasons": {
|
||||
"requires_linux": "এই টুল শুধুমাত্র Linux হোস্টে কাজ করে যেখানে প্রয়োজনীয় ডিভাইস ফাইল উন্মুক্ত আছে।",
|
||||
"requires_serial_platform": "এই টুল বর্তমানে অ্যাক্সেসযোগ্য সিরিয়াল পোর্ট সহ Linux, macOS এবং Windows হোস্ট সমর্থন করে।",
|
||||
"requires_skills": "এই দক্ষতা-রেজিস্ট্রি টুল ব্যবহার করার আগে `tools.skills` সক্ষম করুন।",
|
||||
"requires_subagent": "স্প্যান টুল কাজ অর্পণ করার আগে `tools.subagent` সক্ষম করুন।",
|
||||
"requires_mcp_discovery": "MCP আবিষ্কার টুল উপলব্ধ হওয়ার আগে `tools.mcp.discovery` সক্ষম করুন।",
|
||||
"requires_web_search_provider": "কমপক্ষে একটি প্রস্তুত বাহ্যিক ওয়েব-অনুসন্ধান প্রোভাইডার কনফিগার করুন।"
|
||||
}
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"load_error": "কনফিগারেশন লোড করতে ব্যর্থ। অনুগ্রহ করে রিফ্রেশ করুন এবং আবার চেষ্টা করুন।",
|
||||
"workspace": "ওয়ার্কস্পেস ডিরেক্টরি",
|
||||
"workspace_hint": "এজেন্ট ফাইল অপারেশনের জন্য বেস ডিরেক্টরি।",
|
||||
"restrict_workspace": "ওয়ার্কস্পেসে সীমাবদ্ধ করুন",
|
||||
"restrict_workspace_hint": "শুধুমাত্র ওয়ার্কস্পেসের ভিতরে ফাইল অপারেশন অনুমতি দিন।",
|
||||
"split_on_marker": "চ্যাটি মোড",
|
||||
"split_on_marker_hint": "বাস্তব মানুষের চ্যাটিংয়ের মতো দীর্ঘ বার্তাগুলিকে ছোট বার্তায় বিভক্ত করুন।",
|
||||
"tool_feedback_enabled": "টুল ফিডব্যাক",
|
||||
"tool_feedback_enabled_hint": "প্রতিটি টুল চালানোর আগে বর্তমান চ্যাটে একটি সংক্ষিপ্ত নির্বাহ নোট পাঠান।",
|
||||
"tool_feedback_separate_messages": "পৃথক ফিডব্যাক বার্তা",
|
||||
"tool_feedback_separate_messages_hint": "একটি একক প্লেসহোল্ডার/প্রগ্রেস বার্তা পুনরায় ব্যবহার করার পরিবর্তে প্রতিটি টুল ফিডব্যাক আপডেটকে তার নিজস্ব চ্যাট বার্তা হিসেবে রাখুন।",
|
||||
"tool_feedback_max_args_length": "টুল আর্গস প্রিভিউ দৈর্ঘ্য",
|
||||
"tool_feedback_max_args_length_hint": "প্রতিটি টুল আর্গুমেন্ট প্রিভিউতে দেখানো অক্ষরের সর্বাধিক সংখ্যা। ডিফল্ট ব্যবহার করতে 0 সেট করুন।",
|
||||
"exec_enabled": "কমান্ড অনুমতি দিন",
|
||||
"exec_enabled_hint": "অ্যাপের জন্য কমান্ড নির্বাহ সক্ষম বা নিষ্ক্রিয় করুন। নিষ্ক্রিয় থাকলে, কোনো কমান্ড অনুরোধ চলবে না।",
|
||||
"allow_remote": "রিমোট কমান্ড অনুমতি দিন",
|
||||
"allow_remote_hint": "সক্ষম থাকলে, রিমোট সেশন বা নন-লোকাল প্রসঙ্গও কমান্ড চালাতে পারে। নিষ্ক্রিয় থাকলে, কমান্ড নির্বাহ স্থানীয় নিরাপদ প্রসঙ্গে সীমাবদ্ধ থাকে।",
|
||||
"enable_deny_patterns": "ব্ল্যাকলিস্ট সক্ষম করুন",
|
||||
"enable_deny_patterns_hint": "সক্ষম থাকলে, অ্যাপ্লিকেশন তার বিল্ট-ইন বিপজ্জনক প্যাটার্ন এবং নিচের কাস্টম কমান্ড ব্ল্যাকলিস্টের সাথে মিলে যাওয়া কমান্ডগুলি ব্লক করে।",
|
||||
"exec_timeout_seconds": "কমান্ড টাইমআউট (সেকেন্ড)",
|
||||
"exec_timeout_seconds_hint": "কমান্ড অনুরোধের জন্য সর্বাধিক রানটাইম। ডিফল্ট টাইমআউট ব্যবহার করতে 0 সেট করুন।",
|
||||
"custom_deny_patterns": "কমান্ড ব্ল্যাকলিস্ট",
|
||||
"custom_deny_patterns_hint": "অতিরিক্ত কমান্ড-ব্লকিং নিয়ম যোগ করুন, প্রতি লাইনে একটি রেগুলার এক্সপ্রেশন। এখানের কোনো নিয়মের সাথে মেলে এমন একটি কমান্ড ব্লক করা হবে।",
|
||||
"custom_allow_patterns": "কমান্ড হোয়াইটলিস্ট",
|
||||
"custom_allow_patterns_hint": "অতিরিক্ত কমান্ড-অনুমতি নিয়ম যোগ করুন, প্রতি লাইনে একটি রেগুলার এক্সপ্রেশন। এখানের কোনো নিয়মের সাথে মেলে এমন একটি কমান্ড ব্ল্যাকলিস্ট মিল এড়িয়ে যায়, তবে অন্যান্য নিরাপত্তা সীমা এখনও প্রযোজ্য।",
|
||||
"custom_patterns_placeholder": "^rm\\s+-rf\\b\n^git\\s+push\\b",
|
||||
"pattern_detector_title": "প্যাটার্ন সনাক্তকরণ টুল",
|
||||
"pattern_detector_hint": "যেকোনো ব্ল্যাকলিস্ট বা হোয়াইটলিস্ট প্যাটার্নের সাথে মেলে কিনা তা পরীক্ষা করতে একটি কমান্ড লিখুন।",
|
||||
"pattern_detector_input_placeholder": "পরীক্ষার জন্য একটি কমান্ড লিখুন, যেমন rm -rf /tmp",
|
||||
"pattern_detector_test_button": "পরীক্ষা",
|
||||
"pattern_detector_result_allowed": "অনুমোদিত (হোয়াইটলিস্টের সাথে মেলে)",
|
||||
"pattern_detector_result_blocked": "ব্লক করা (ব্ল্যাকলিস্টের সাথে মেলে)",
|
||||
"pattern_detector_result_no_match": "কোনো মিল নেই (ডিফল্ট নিয়ম ব্যবহার করবে)",
|
||||
"allow_shell_execution": "সময়সূচী কমান্ড অনুমতি দিন",
|
||||
"allow_shell_execution_hint": "ডিফল্টরূপে সময়সূচী কাজগুলিকে কমান্ড চালানোর অনুমতি দিন। নিষ্ক্রিয় থাকলে, ব্যবহারকারীদের একটি কমান্ড কাজের সময়সূচী করতে command_confirm=true পাস করতে হবে।",
|
||||
"cron_exec_timeout": "সময়সূচী কমান্ড টাইমআউট (মিনিট)",
|
||||
"cron_exec_timeout_hint": "সময়সূচী কমান্ডের জন্য সর্বাধিক রানটাইম। টাইমআউট নিষ্ক্রিয় করতে 0 সেট করুন।",
|
||||
"max_tokens": "ম্যাক্স টোকেন",
|
||||
"max_tokens_hint": "প্রতি মডেল প্রতিক্রিয়ার জন্য উপরের টোকেন সীমা।",
|
||||
"context_window": "প্রসঙ্গ উইন্ডো",
|
||||
"context_window_hint": "টোকেনে মডেল ইনপুট প্রসঙ্গ ধারণক্ষমতা। ডিফল্ট ব্যবহার করতে খালি রাখুন (4x ম্যাক্স টোকেন)।",
|
||||
"max_tool_iterations": "ম্যাক্স টুল ইটারেশন",
|
||||
"max_tool_iterations_hint": "একটি একক কাজের মধ্যে সর্বাধিক টুল-কল লুপ।",
|
||||
"summarize_threshold": "সংক্ষিপ্তসারের বার্তা থ্রেশহোল্ড",
|
||||
"summarize_threshold_hint": "এই সংখ্যক বার্তার পরে সংক্ষিপ্তসার শুরু করুন।",
|
||||
"summarize_token_percent": "সংক্ষিপ্তসারের টোকেন শতাংশ",
|
||||
"summarize_token_percent_hint": "কথোপকথন সংক্ষিপ্তসার ট্রিগার হলে ব্যবহৃত।",
|
||||
"turn_profile": "অনুরোধ প্রসঙ্গ নীতি",
|
||||
"turn_profile_hint": "প্রতিটি অনুরোধে কী প্রসঙ্গ বহন করে তা নিয়ন্ত্রণ করে। স্বাভাবিক চ্যাট আচরণ রাখতে নিষ্ক্রিয় রাখুন।",
|
||||
"turn_profile_enabled": "নীতি সক্ষম করুন",
|
||||
"turn_profile_enabled_hint": "সক্ষম থাকলে, এই নীতি প্রতিটি নতুন টার্নে প্রযোজ্য। নিষ্ক্রিয় থাকলে, PicoClaw মূল প্রসঙ্গ আচরণ ব্যবহার করে।",
|
||||
"turn_profile_mode_default": "ডিফল্ট",
|
||||
"turn_profile_mode_off": "বন্ধ",
|
||||
"turn_profile_mode_custom": "অনুমোদিত তালিকা",
|
||||
"turn_profile_history": "ইতিহাস প্রসঙ্গ",
|
||||
"turn_profile_history_hint": "ডিফল্ট এই সেশনের পূর্ববর্তী বার্তা অন্তর্ভুক্ত করে। বন্ধ করলে টার্নটি একটি নতুন চ্যাটের মতো আচরণ করে এবং তার ফলাফল ইতিহাসে সংরক্ষণ করা এড়িয়ে যায়।",
|
||||
"turn_profile_system_prompt": "সিস্টেম প্রসঙ্গ",
|
||||
"turn_profile_system_prompt_hint": "ডিফল্ট PicoClaw পরিচয়, ওয়ার্কস্পেস, মেমরি এবং রানটাইম নির্দেশাবলী অন্তর্ভুক্ত করে। বন্ধ করলে অনুরোধ দ্বারা স্পষ্টভাবে সরবরাহিত সিস্টেম প্রম্পটগুলিই রাখা হয়।",
|
||||
"turn_profile_skills": "দক্ষতা প্রম্পট",
|
||||
"turn_profile_skills_hint": "ডিফল্ট উপলব্ধ দক্ষতা এবং সক্রিয় দক্ষতা নির্দেশাবলী অন্তর্ভুক্ত করে। বন্ধ করলে সেগুলি লুকায়। অনুমোদিত তালিকা প্রতি লাইনে এক করে প্রবেশ করানো দক্ষতার নামগুলিই রাখে।",
|
||||
"turn_profile_skills_allow_placeholder": "skill-name\nanother-skill",
|
||||
"turn_profile_tools": "কলযোগ্য টুল",
|
||||
"turn_profile_tools_hint": "ডিফল্ট স্বাভাবিক টুল উন্মুক্ত করে। বন্ধ টুল কলগুলি প্রতিরোধ করে। অনুমোদিত তালিকা প্রতি লাইনে এক করে প্রবেশ করানো টুলের নামগুলিই রাখে, যেমন web_search।",
|
||||
"turn_profile_tools_allow_placeholder": "web_search\nweb_fetch",
|
||||
"session_scope": "সেশন স্কোপ",
|
||||
"session_scope_hint": "পিয়ার/চ্যানেল জুড়ে চ্যাট প্রসঙ্গ কীভাবে বিচ্ছিন্ন করা হয়।",
|
||||
"session_scope_per_channel_peer": "প্রতি চ্যানেল + পিয়ার",
|
||||
"session_scope_per_channel_peer_desc": "প্রতিটি চ্যানেলে প্রতিটি ব্যবহারকারীর জন্য আলাদা প্রসঙ্গ।",
|
||||
"session_scope_per_channel": "প্রতি চ্যানেল",
|
||||
"session_scope_per_channel_desc": "প্রতি চ্যানেলে একটি শেয়ার করা প্রসঙ্গ।",
|
||||
"session_scope_per_peer": "প্রতি পিয়ার",
|
||||
"session_scope_per_peer_desc": "চ্যানেল জুড়ে প্রতি ব্যবহারকারীর জন্য একটি প্রসঙ্গ।",
|
||||
"session_scope_global": "গ্লোবাল",
|
||||
"session_scope_global_desc": "সব বার্তা একটি গ্লোবাল প্রসঙ্গ শেয়ার করে।",
|
||||
"heartbeat_enabled": "হার্টবিট",
|
||||
"heartbeat_enabled_hint": "পর্যায়ক্রমিক হার্টবিট বার্তা পাঠান।",
|
||||
"heartbeat_interval": "হার্টবিট ব্যবধান (মিনিট)",
|
||||
"heartbeat_interval_hint": "হার্টবিট সংকেতের মধ্যে মিনিটে ব্যবধান।",
|
||||
"devices_enabled": "ডিভাইস সক্ষম করুন",
|
||||
"devices_enabled_hint": "হার্ডওয়্যার-ডিভাইস ইন্টিগ্রেশন সক্ষম করুন।",
|
||||
"monitor_usb": "USB মনিটর করুন",
|
||||
"monitor_usb_hint": "ডিভাইস সক্ষম থাকলে USB প্লাগ/আনপ্লাগ ইভেন্ট দেখুন।",
|
||||
"autostart_label": "লগইনে চালু করুন",
|
||||
"autostart_hint": "লগ ইন করার সময় স্বয়ংক্রিয়ভাবে PicoClaw Web চালু করুন।",
|
||||
"autostart_unsupported": "এই প্ল্যাটফর্মে লগইনে চালু করা সমর্থিত নয়।",
|
||||
"autostart_load_error": "লগইনে চালু করার স্ট্যাটাস লোড করতে ব্যর্থ।",
|
||||
"server_port": "সার্ভিস পোর্ট",
|
||||
"server_port_hint": "PicoClaw Web দ্বারা ব্যবহৃত HTTP পোর্ট।",
|
||||
"launcher_section_hint": "এই বিভাগের পরিবর্তন লঞ্চার পুনরায় চালু হওয়ার পরে কার্যকর হয়।",
|
||||
"gateway_restart_hint": "এই বিভাগের পরিবর্তন গেটওয়ে পুনরায় চালু হওয়ার পরে কার্যকর হয়।",
|
||||
"dashboard_password": "লগইন পাসওয়ার্ড",
|
||||
"dashboard_password_hint": "একটি নতুন লগইন পাসওয়ার্ড সেট করুন।",
|
||||
"dashboard_password_placeholder": "কমপক্ষে ৮ অক্ষর",
|
||||
"dashboard_password_confirm": "নতুন পাসওয়ার্ড নিশ্চিত করুন",
|
||||
"dashboard_password_confirm_hint": "নতুন লগইন পাসওয়ার্ড আবার লিখুন।",
|
||||
"dashboard_password_confirm_placeholder": "পাসওয়ার্ড আবার লিখুন",
|
||||
"dashboard_password_required": "নতুন লগইন পাসওয়ার্ড লিখুন এবং নিশ্চিত করুন।",
|
||||
"dashboard_password_mismatch": "লগইন পাসওয়ার্ড মিলছে না।",
|
||||
"dashboard_password_min_length": "লগইন পাসওয়ার্ড কমপক্ষে ৮ অক্ষরের হতে হবে।",
|
||||
"lan_access": "LAN অ্যাক্সেস সক্ষম করুন",
|
||||
"lan_access_hint": "আপনার স্থানীয় নেটওয়ার্কের অন্যান্য ডিভাইস থেকে অ্যাক্সেসের অনুমতি দিন।",
|
||||
"allowed_cidrs": "অনুমোদিত নেটওয়ার্ক CIDR",
|
||||
"allowed_cidrs_hint": "শুধুমাত্র এই CIDR পরিসরের ক্লায়েন্টরা সার্ভিস অ্যাক্সেস করতে পারে। প্রতি লাইনে একটি বা কমা দিয়ে আলাদা। সবাইকে অনুমতি দিতে খালি রাখুন।",
|
||||
"allowed_cidrs_placeholder": "192.168.1.0/24\n10.0.0.0/8",
|
||||
"evolution_section_hint": "এজেন্টকে সম্পন্ন টার্ন থেকে শিখতে দিন এবং দক্ষতার উন্নতি প্রস্তুত করুন।",
|
||||
"evolution_enabled": "ইভোলিউশন সক্ষম করুন",
|
||||
"evolution_enabled_hint": "সম্পন্ন টার্নের জন্য শেখার ডেটা রেকর্ড করুন। ড্রাফ্ট এবং অ্যাপ্লাই মোড দক্ষতা আপডেটও তৈরি করতে পারে।",
|
||||
"evolution_mode": "ইভোলিউশন মোড",
|
||||
"evolution_mode_hint": "Observe শুধু ডেটা রেকর্ড করে, Draft প্রার্থী দক্ষতা প্রস্তুত করে, Apply ওয়ার্কস্পেস দক্ষতায় গৃহীত ড্রাফ্ট লিখতে পারে।",
|
||||
"evolution_mode_observe": "পর্যবেক্ষণ",
|
||||
"evolution_mode_draft": "ড্রাফ্ট",
|
||||
"evolution_mode_apply": "প্রয়োগ",
|
||||
"evolution_state_dir": "স্টেট ডিরেক্টরি",
|
||||
"evolution_state_dir_hint": "ইভোলিউশন স্টেটের জন্য ঐচ্ছিক ডিরেক্টরি। ওয়ার্কস্পেস ডিফল্ট ব্যবহার করতে খালি রাখুন।",
|
||||
"evolution_min_task_count": "ন্যূনতম কাজ গণনা",
|
||||
"evolution_min_task_count_hint": "একটি প্যাটার্ন একটি ড্রাফ্ট তৈরি করার আগে প্রয়োজনীয় ন্যূনতম সম্পর্কিত কাজ।",
|
||||
"evolution_min_success_ratio": "ন্যূনতম সাফল্যের অনুপাত",
|
||||
"evolution_min_success_ratio_hint": "ক্লাস্টার্ড কাজের জন্য প্রয়োজনীয় সাফল্যের অনুপাত। 0 এর চেয়ে বড় এবং 1 পর্যন্ত একটি মান ব্যবহার করুন।",
|
||||
"evolution_cold_path_trigger": "কোল্ড পাথ ট্রিগার",
|
||||
"evolution_cold_path_trigger_hint": "যোগ্য শেখার রেকর্ডের জন্য ড্রাফ্ট জেনারেশন কখন চলবে তা চয়ন করুন।",
|
||||
"evolution_cold_path_after_turn": "প্রতিটি টার্নের পরে",
|
||||
"evolution_cold_path_scheduled": "সময়সূচী",
|
||||
"evolution_cold_path_manual": "বন্ধ",
|
||||
"evolution_cold_path_times": "সময়সূচী সময়",
|
||||
"evolution_cold_path_times_hint": "সময়সূচী কোল্ড-পাথ প্রক্রিয়াকরণের জন্য চালানোর সময়। প্রতি লাইনে একটি HH:MM মান লিখুন।",
|
||||
"mcp_section_hint": "ম্যানুয়ালি config.json সম্পাদনা না করে MCP সার্ভার কনফিগার করুন।",
|
||||
"mcp_enabled": "MCP সক্ষম করুন",
|
||||
"mcp_enabled_hint": "MCP সার্ভার ইন্টিগ্রেশন চালু বা বন্ধ করুন।",
|
||||
"mcp_discovery_enabled": "MCP আবিষ্কার সক্ষম করুন",
|
||||
"mcp_discovery_enabled_hint": "MCP আবিষ্কার টুলকে নিবন্ধিত MCP সার্ভার অনুসন্ধান করার অনুমতি দিন।",
|
||||
"mcp_discovery_ttl": "আবিষ্কৃত টুল আনলক TTL",
|
||||
"mcp_discovery_ttl_hint": "অনুসন্ধানের পরে আবিষ্কৃত টুলগুলি কত টুল-নির্বাহ TTL টিক উপলব্ধ থাকবে।",
|
||||
"mcp_discovery_max_results": "আবিষ্কার সর্বাধিক ফলাফল",
|
||||
"mcp_discovery_max_results_hint": "প্রতি কোয়েরিতে ফিরিয়ে দেওয়া সর্বাধিক MCP আবিষ্কার মিল।",
|
||||
"mcp_discovery_use_bm25": "BM25 র্যাঙ্কিং ব্যবহার করুন",
|
||||
"mcp_discovery_use_bm25_hint": "MCP আবিষ্কার ফলাফলের জন্য BM25 লেক্সিকাল স্কোরিং ব্যবহার করুন।",
|
||||
"mcp_discovery_use_regex": "রেগেক্স অনুসন্ধান সক্ষম করুন",
|
||||
"mcp_discovery_use_regex_hint": "MCP আবিষ্কারে রেগেক্স-ভিত্তিক মিলের অনুমতি দিন।",
|
||||
"mcp_servers": "MCP সার্ভার",
|
||||
"mcp_servers_hint": "MCP সার্ভার যোগ, সম্পাদনা বা সরান।",
|
||||
"mcp_server_new": "নতুন MCP সার্ভার",
|
||||
"mcp_server_add": "সার্ভার যোগ করুন",
|
||||
"mcp_server_remove": "সরান",
|
||||
"mcp_server_enabled": "সক্ষম",
|
||||
"mcp_server_discovery_mode": "আবিষ্কার মোড",
|
||||
"mcp_server_discovery_mode_inherit": "গ্লোবাল আবিষ্কার মোড অনুসরণ করুন",
|
||||
"mcp_server_discovery_mode_deferred": "বিলম্বিত আবিষ্কার",
|
||||
"mcp_server_discovery_mode_eager": "ইগার নিবন্ধন",
|
||||
"mcp_server_name_placeholder": "সার্ভারের নাম (যেমন github)",
|
||||
"mcp_server_url_placeholder": "সার্ভার URL (যেমন https://example.com/mcp)",
|
||||
"mcp_server_command_placeholder": "কমান্ড (যেমন npx)",
|
||||
"mcp_server_env_file_placeholder": "পরিবেশ ফাইল পাথ (ঐচ্ছিক)",
|
||||
"mcp_server_args_placeholder": "আর্গস, প্রতি লাইনে একটি",
|
||||
"mcp_server_env_placeholder": "পরিবেশ JSON অবজেক্ট",
|
||||
"mcp_server_headers_placeholder": "হেডার JSON অবজেক্ট",
|
||||
"sections": {
|
||||
"agent": "এজেন্ট",
|
||||
"runtime": "রানটাইম",
|
||||
"evolution": "ইভোলিউশন",
|
||||
"mcp": "MCP",
|
||||
"exec": "কমান্ড চালান",
|
||||
"cron": "ক্রন কাজ",
|
||||
"launcher": "লঞ্চার",
|
||||
"devices": "ডিভাইস"
|
||||
},
|
||||
"open_raw": "র কনফিগ",
|
||||
"back_to_visual": "ভিজ্যুয়াল কনফিগ",
|
||||
"raw_json_title": "র JSON কনফিগারেশন",
|
||||
"json_placeholder": "বৈধ JSON কনফিগারেশন লিখুন...",
|
||||
"save_success": "কনফিগারেশন সফলভাবে সংরক্ষিত হয়েছে।",
|
||||
"save_error": "কনফিগারেশন সংরক্ষণ করতে ব্যর্থ।",
|
||||
"reset_confirm_title": "পরিবর্তন রিসেট করুন",
|
||||
"reset_confirm_desc": "আপনি কি আপনার অসংরক্ষিত পরিবর্তনগুলি সর্বশেষ সংরক্ষিত অবস্থায় রিসেট করতে চান?",
|
||||
"reset_success": "পরিবর্তনগুলি সর্বশেষ সংরক্ষিত অবস্থায় রিসেট করা হয়েছে।",
|
||||
"invalid_json": "অবৈধ JSON ফর্ম্যাট।",
|
||||
"format_success": "JSON সফলভাবে ফর্ম্যাট করা হয়েছে।",
|
||||
"format_error": "অবৈধ JSON ফর্ম্যাট।",
|
||||
"format": "ফর্ম্যাট",
|
||||
"unsaved_changes": "আপনার অসংরক্ষিত পরিবর্তন রয়েছে।",
|
||||
"factory_reset": "ফ্যাক্টরি রিসেট",
|
||||
"factory_reset_confirm_title": "ফ্যাক্টরি ডিফল্টে রিসেট করুন",
|
||||
"factory_reset_confirm_desc": "এটি সমস্ত কনফিগারেশন ফ্যাক্টরি ডিফল্টে রিসেট করবে। API কী এবং নিরাপত্তা ক্রেডেনশিয়াল সংরক্ষিত থাকবে। বর্তমান কনফিগারেশনের একটি ব্যাকআপ তৈরি করা হবে।",
|
||||
"factory_reset_confirm": "ডিফল্টে রিসেট করুন",
|
||||
"factory_reset_success": "কনফিগারেশন ফ্যাক্টরি ডিফল্টে রিসেট করা হয়েছে।",
|
||||
"factory_reset_error": "কনফিগারেশন রিসেট করতে ব্যর্থ।"
|
||||
},
|
||||
"logs": {
|
||||
"log_level_error": "লগ লেভেল আপডেট করতে ব্যর্থ।",
|
||||
"clear": "লগ পরিষ্কার করুন",
|
||||
"empty": "লগের জন্য অপেক্ষা করা হচ্ছে..."
|
||||
}
|
||||
},
|
||||
"tour": {
|
||||
"skip": "ট্যুর এড়িয়ে যান",
|
||||
"prev": "পূর্ববর্তী",
|
||||
"next": "পরবর্তী",
|
||||
"finish": "শেষ করুন",
|
||||
"welcome": {
|
||||
"title": "PicoClaw-এ স্বাগতম",
|
||||
"description": "PicoClaw একটি শক্তিশালী AI সহকারী প্ল্যাটফর্ম। মৌলিক সেটআপ সম্পন্ন করতে আপনাকে সাহায্য করতে কয়েক সেকেন্ড সময় নিই।"
|
||||
},
|
||||
"models": {
|
||||
"title": "মডেল কনফিগার করুন",
|
||||
"description": "AI প্রোভাইডারদের জন্য API কী কনফিগার করতে বাঁ দিকের \"মডেল\" মেনুতে ক্লিক করুন। শুধুমাত্র কনফিগার করা মডেলগুলি চ্যাটের জন্য ব্যবহার করা যেতে পারে।"
|
||||
},
|
||||
"gateway": {
|
||||
"title": "গেটওয়ে চালু করুন",
|
||||
"description": "মডেল কনফিগার করার পরে, AI-এর সাথে চ্যাট শুরু করতে উপরের \"গেটওয়ে চালু করুন\" বোতামে ক্লিক করুন।"
|
||||
},
|
||||
"docs": {
|
||||
"title": "ডকুমেন্টেশন দেখুন",
|
||||
"description": "আরও সাহায্যের প্রয়োজন? বিস্তারিত গাইড এবং কনফিগারেশন ডকুমেন্ট দেখতে উপরের ডান কোণে ডকুমেন্টেশন বোতামে ক্লিক করুন।"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,8 +95,9 @@
|
||||
"contextTitle": "Kontext",
|
||||
"contextDetail": "Zobrazit detail",
|
||||
"attachImage": "Přidat obrázky",
|
||||
"dropImagesActive": "Uvolněním přidáte obrázky",
|
||||
"removeImage": "Odebrat obrázek",
|
||||
"uploadedImage": "Nahraný obrázek",
|
||||
"uploadedImage": "Přiložený obrázek",
|
||||
"invalidImage": "\"{{name}}\" není podporovaný formát obrázku.",
|
||||
"imageTooLarge": "\"{{name}}\" překračuje limit {{size}}.",
|
||||
"imageReadFailed": "Čtení souboru \"{{name}}\" selhalo.",
|
||||
|
||||
@@ -97,8 +97,9 @@
|
||||
"contextTitle": "Context",
|
||||
"contextDetail": "View Details",
|
||||
"attachImage": "Add images",
|
||||
"dropImagesActive": "Release to add images",
|
||||
"removeImage": "Remove image",
|
||||
"uploadedImage": "Uploaded image",
|
||||
"uploadedImage": "Attached image",
|
||||
"invalidImage": "\"{{name}}\" is not a supported image file.",
|
||||
"imageTooLarge": "\"{{name}}\" exceeds the {{size}} limit.",
|
||||
"imageReadFailed": "Failed to read \"{{name}}\".",
|
||||
|
||||
@@ -97,8 +97,9 @@
|
||||
"contextTitle": "Contexto",
|
||||
"contextDetail": "Ver Detalhes",
|
||||
"attachImage": "Adicionar imagens",
|
||||
"dropImagesActive": "Solte para adicionar imagens",
|
||||
"removeImage": "Remover imagem",
|
||||
"uploadedImage": "Imagem enviada",
|
||||
"uploadedImage": "Imagem anexada",
|
||||
"invalidImage": "\"{{name}}\" não é um arquivo de imagem suportado.",
|
||||
"imageTooLarge": "\"{{name}}\" excede o limite de {{size}}.",
|
||||
"imageReadFailed": "Falha ao ler \"{{name}}\".",
|
||||
|
||||
@@ -97,8 +97,9 @@
|
||||
"contextTitle": "上下文",
|
||||
"contextDetail": "查看详情",
|
||||
"attachImage": "添加图片",
|
||||
"dropImagesActive": "松开以添加图片",
|
||||
"removeImage": "移除图片",
|
||||
"uploadedImage": "已上传图片",
|
||||
"uploadedImage": "已添加图片",
|
||||
"invalidImage": "“{{name}}”不是支持的图片文件。",
|
||||
"imageTooLarge": "“{{name}}”超过了 {{size}} 限制。",
|
||||
"imageReadFailed": "读取“{{name}}”失败。",
|
||||
|
||||
Reference in New Issue
Block a user