move secrets to env variables add palworld commands
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 37s
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 37s
This commit is contained in:
@@ -1,2 +1,4 @@
|
|||||||
# Copy this file to .env and set your real values.
|
# Copy this file to .env and set your real values.
|
||||||
DISCORD_BOT_TOKEN=YOUR_DISCORD_BOT_TOKEN_HERE
|
DISCORD_BOT_TOKEN=YOUR_DISCORD_BOT_TOKEN_HERE
|
||||||
|
PALWORLD_PASSWORD=YOUR_PALWORLD_PASSWORD_HERE
|
||||||
|
LLM_API_KEY=YOUR_LLM_API_KEY_HERE
|
||||||
27
.gitea/workflows/docker-publish.yml
Normal file
27
.gitea/workflows/docker-publish.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
name: Build and Publish Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: git.gpatti.com
|
||||||
|
IMAGE: git.gpatti.com/gpatti/tentaclebot
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Log in to Gitea Container Registry
|
||||||
|
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.gpatti.com -u gpatti --password-stdin
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ env.IMAGE }}:latest
|
||||||
|
${{ env.IMAGE }}:${{ github.sha }}
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -28,10 +28,6 @@ bld/
|
|||||||
*.userosscache
|
*.userosscache
|
||||||
*.sln.docstates
|
*.sln.docstates
|
||||||
|
|
||||||
# Configuration files with sensitive data
|
|
||||||
appsettings.json
|
|
||||||
!appsettings.*.json
|
|
||||||
|
|
||||||
# NuGet
|
# NuGet
|
||||||
.nuget/
|
.nuget/
|
||||||
*.nupkg
|
*.nupkg
|
||||||
|
|||||||
14
AGENTS.md
14
AGENTS.md
@@ -10,7 +10,7 @@ docker compose logs -f # View Docker logs
|
|||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
1. Set `DISCORD_BOT_TOKEN` env var (preferred) or edit `appsettings.json`
|
1. Set `DISCORD_BOT_TOKEN` env var — secrets must not be stored in `appsettings.json`
|
||||||
2. Copy `.env.example` to `.env` for Docker: `cp .env.example .env`
|
2. Copy `.env.example` to `.env` for Docker: `cp .env.example .env`
|
||||||
3. `appsettings.json` is gitignored — never commit it
|
3. `appsettings.json` is gitignored — never commit it
|
||||||
|
|
||||||
@@ -22,6 +22,18 @@ docker compose logs -f # View Docker logs
|
|||||||
|
|
||||||
Add new commands by creating a class in `Commands/` that extends `InteractionModuleBase<SocketInteractionContext>`. Commands are auto-discovered via `AddModulesAsync(typeof(Program).Assembly)`.
|
Add new commands by creating a class in `Commands/` that extends `InteractionModuleBase<SocketInteractionContext>`. Commands are auto-discovered via `AddModulesAsync(typeof(Program).Assembly)`.
|
||||||
|
|
||||||
|
## Secrets
|
||||||
|
|
||||||
|
All secrets (API keys, passwords, tokens) must be read from environment variables — never from `appsettings.json`. Use `Environment.GetEnvironmentVariable()` directly in service constructors.
|
||||||
|
|
||||||
|
| Secret | Env Var |
|
||||||
|
|---|---|
|
||||||
|
| Discord bot token | `DISCORD_BOT_TOKEN` |
|
||||||
|
| LLM API key | `LLM_API_KEY` |
|
||||||
|
| Palworld server password | `PALWORLD_PASSWORD` |
|
||||||
|
|
||||||
|
Non-secret configuration (URLs, model names, numeric settings) may stay in `appsettings.json` via `IConfiguration`.
|
||||||
|
|
||||||
## Gotchas
|
## Gotchas
|
||||||
|
|
||||||
- **Two `DiscordSocketClient` instances** are created in `Program.cs` — one for the bot client and one passed into `InteractionService`. They are separate instances sharing the same config.
|
- **Two `DiscordSocketClient` instances** are created in `Program.cs` — one for the bot client and one passed into `InteractionService`. They are separate instances sharing the same config.
|
||||||
|
|||||||
250
Commands/PalworldCommands.cs
Normal file
250
Commands/PalworldCommands.cs
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Discord;
|
||||||
|
using Discord.Interactions;
|
||||||
|
using TentacleBot.Services;
|
||||||
|
|
||||||
|
namespace TentacleBot.Commands;
|
||||||
|
|
||||||
|
[Group("palworld", "Palworld server status commands")]
|
||||||
|
public class PalworldCommands : InteractionModuleBase<SocketInteractionContext>
|
||||||
|
{
|
||||||
|
private readonly PalworldApiService _palworldApi;
|
||||||
|
|
||||||
|
public PalworldCommands(PalworldApiService palworldApi)
|
||||||
|
{
|
||||||
|
_palworldApi = palworldApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SlashCommand("status", "Show a combined server status dashboard")]
|
||||||
|
public async Task HandleStatusAsync()
|
||||||
|
{
|
||||||
|
await DeferAsync();
|
||||||
|
|
||||||
|
var infoTask = _palworldApi.GetInfoAsync();
|
||||||
|
var metricsTask = _palworldApi.GetMetricsAsync();
|
||||||
|
var playersTask = _palworldApi.GetPlayersAsync();
|
||||||
|
|
||||||
|
var info = await infoTask;
|
||||||
|
var metrics = await metricsTask;
|
||||||
|
var players = await playersTask;
|
||||||
|
|
||||||
|
if (info is null && metrics is null && players is null)
|
||||||
|
{
|
||||||
|
await FollowupAsync(embed: new EmbedBuilder()
|
||||||
|
.WithTitle("Palworld Server Status")
|
||||||
|
.WithDescription("Failed to connect to the Palworld server. Check that the server is running and the REST API is enabled.")
|
||||||
|
.WithColor(Color.Red)
|
||||||
|
.WithTimestamp(DateTimeOffset.UtcNow)
|
||||||
|
.Build());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var embed = new EmbedBuilder()
|
||||||
|
.WithTitle("Palworld Server Status")
|
||||||
|
.WithColor(Color.DarkGreen)
|
||||||
|
.WithTimestamp(DateTimeOffset.UtcNow);
|
||||||
|
|
||||||
|
if (info is not null)
|
||||||
|
{
|
||||||
|
var root = info.RootElement;
|
||||||
|
var name = GetString(root, "servername");
|
||||||
|
var version = GetString(root, "version");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(name))
|
||||||
|
embed.AddField("Server Name", name, true);
|
||||||
|
if (!string.IsNullOrWhiteSpace(version))
|
||||||
|
embed.AddField("Version", version, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metrics is not null)
|
||||||
|
{
|
||||||
|
var root = metrics.RootElement;
|
||||||
|
embed.AddField("Players", $"{GetInt(root, "currentplayernum")} / {GetInt(root, "maxplayernum")}", true);
|
||||||
|
embed.AddField("FPS", $"{GetDouble(root, "serverfps")}", true);
|
||||||
|
|
||||||
|
var uptime = GetInt(root, "uptime");
|
||||||
|
embed.AddField("Uptime", FormatUptime(uptime), true);
|
||||||
|
|
||||||
|
var days = GetInt(root, "days");
|
||||||
|
embed.AddField("In-Game Days", $"{days}", true);
|
||||||
|
|
||||||
|
var camps = GetInt(root, "basecampnum");
|
||||||
|
embed.AddField("Base Camps", $"{camps}", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (players is not null)
|
||||||
|
{
|
||||||
|
var playerList = players.RootElement.GetProperty("players");
|
||||||
|
var count = playerList.GetArrayLength();
|
||||||
|
|
||||||
|
if (count > 0)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
var limit = Math.Min(count, 15);
|
||||||
|
for (var i = 0; i < limit; i++)
|
||||||
|
{
|
||||||
|
var p = playerList[i];
|
||||||
|
var name = GetString(p, "name");
|
||||||
|
var level = GetInt(p, "level");
|
||||||
|
sb.AppendLine($"**{name}** (Lv.{level})");
|
||||||
|
}
|
||||||
|
if (count > 15)
|
||||||
|
sb.AppendLine($"*...and {count - 15} more*");
|
||||||
|
embed.AddField($"Online Players ({count})", sb.ToString());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
embed.AddField("Online Players", "None");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await FollowupAsync(embed: embed.Build());
|
||||||
|
}
|
||||||
|
|
||||||
|
[SlashCommand("info", "Get Palworld server information")]
|
||||||
|
public async Task HandleInfoAsync()
|
||||||
|
{
|
||||||
|
await DeferAsync();
|
||||||
|
|
||||||
|
var data = await _palworldApi.GetInfoAsync();
|
||||||
|
|
||||||
|
if (data is null)
|
||||||
|
{
|
||||||
|
await FollowupAsync(embed: ErrorEmbed("info"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var root = data.RootElement;
|
||||||
|
var embed = new EmbedBuilder()
|
||||||
|
.WithTitle("Server Info")
|
||||||
|
.WithColor(Color.Blue)
|
||||||
|
.AddField("Server Name", GetString(root, "servername") ?? "N/A", true)
|
||||||
|
.AddField("Version", GetString(root, "version") ?? "N/A", true)
|
||||||
|
.AddField("Description", GetString(root, "description") ?? "N/A")
|
||||||
|
.AddField("World GUID", GetString(root, "worldguid") ?? "N/A")
|
||||||
|
.WithTimestamp(DateTimeOffset.UtcNow)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
await FollowupAsync(embed: embed);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SlashCommand("players", "Get the list of online players")]
|
||||||
|
public async Task HandlePlayersAsync()
|
||||||
|
{
|
||||||
|
await DeferAsync();
|
||||||
|
|
||||||
|
var data = await _palworldApi.GetPlayersAsync();
|
||||||
|
|
||||||
|
if (data is null)
|
||||||
|
{
|
||||||
|
await FollowupAsync(embed: ErrorEmbed("players"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var players = data.RootElement.GetProperty("players");
|
||||||
|
var embed = new EmbedBuilder()
|
||||||
|
.WithTitle("Online Players")
|
||||||
|
.WithColor(Color.Green)
|
||||||
|
.WithTimestamp(DateTimeOffset.UtcNow);
|
||||||
|
|
||||||
|
var count = players.GetArrayLength();
|
||||||
|
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
embed.WithDescription("No players are currently online.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
var limit = Math.Min(count, 20);
|
||||||
|
for (var i = 0; i < limit; i++)
|
||||||
|
{
|
||||||
|
var p = players[i];
|
||||||
|
var name = GetString(p, "name");
|
||||||
|
var level = GetInt(p, "level");
|
||||||
|
var ping = GetDouble(p, "ping");
|
||||||
|
sb.AppendLine($"{i + 1}. **{name}** — Lv.{level} — {ping:F0}ms ping");
|
||||||
|
}
|
||||||
|
if (count > 20)
|
||||||
|
sb.AppendLine($"*...and {count - 20} more*");
|
||||||
|
embed.WithDescription(sb.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
await FollowupAsync(embed: embed.Build());
|
||||||
|
}
|
||||||
|
|
||||||
|
[SlashCommand("metrics", "Get Palworld server performance metrics")]
|
||||||
|
public async Task HandleMetricsAsync()
|
||||||
|
{
|
||||||
|
await DeferAsync();
|
||||||
|
|
||||||
|
var data = await _palworldApi.GetMetricsAsync();
|
||||||
|
|
||||||
|
if (data is null)
|
||||||
|
{
|
||||||
|
await FollowupAsync(embed: ErrorEmbed("metrics"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var root = data.RootElement;
|
||||||
|
|
||||||
|
var embed = new EmbedBuilder()
|
||||||
|
.WithTitle("Server Metrics")
|
||||||
|
.WithColor(Color.Orange)
|
||||||
|
.AddField("FPS", $"{GetDouble(root, "serverfps"):F1}", true)
|
||||||
|
.AddField("Frame Time", $"{GetDouble(root, "serverframetime"):F2}ms", true)
|
||||||
|
.AddField("Players", $"{GetInt(root, "currentplayernum")} / {GetInt(root, "maxplayernum")}", true)
|
||||||
|
.AddField("Uptime", FormatUptime(GetInt(root, "uptime")), true)
|
||||||
|
.AddField("In-Game Days", $"{GetInt(root, "days")}", true)
|
||||||
|
.AddField("Base Camps", $"{GetInt(root, "basecampnum")}", true)
|
||||||
|
.WithTimestamp(DateTimeOffset.UtcNow)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
await FollowupAsync(embed: embed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatUptime(int seconds)
|
||||||
|
{
|
||||||
|
if (seconds <= 0) return "N/A";
|
||||||
|
|
||||||
|
var ts = TimeSpan.FromSeconds(seconds);
|
||||||
|
var parts = new List<string>();
|
||||||
|
if (ts.Days > 0) parts.Add($"{ts.Days}d");
|
||||||
|
if (ts.Hours > 0) parts.Add($"{ts.Hours}h");
|
||||||
|
if (ts.Minutes > 0) parts.Add($"{ts.Minutes}m");
|
||||||
|
if (parts.Count == 0) parts.Add($"{ts.Seconds}s");
|
||||||
|
return string.Join(" ", parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Embed ErrorEmbed(string endpoint)
|
||||||
|
{
|
||||||
|
return new EmbedBuilder()
|
||||||
|
.WithTitle($"Failed to fetch {endpoint}")
|
||||||
|
.WithDescription("Could not connect to the Palworld server. Verify the server is running and REST API is enabled.")
|
||||||
|
.WithColor(Color.Red)
|
||||||
|
.WithTimestamp(DateTimeOffset.UtcNow)
|
||||||
|
.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? GetString(JsonElement element, string property)
|
||||||
|
{
|
||||||
|
return element.TryGetProperty(property, out var prop) && prop.ValueKind == JsonValueKind.String
|
||||||
|
? prop.GetString()
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetInt(JsonElement element, string property)
|
||||||
|
{
|
||||||
|
return element.TryGetProperty(property, out var prop) && prop.ValueKind == JsonValueKind.Number
|
||||||
|
? prop.GetInt32()
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double GetDouble(JsonElement element, string property)
|
||||||
|
{
|
||||||
|
return element.TryGetProperty(property, out var prop) && prop.ValueKind == JsonValueKind.Number
|
||||||
|
? prop.GetDouble()
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,7 +32,9 @@ var services = new ServiceCollection()
|
|||||||
UseCompiledLambda = true,
|
UseCompiledLambda = true,
|
||||||
}))
|
}))
|
||||||
.AddSingleton<BotService>()
|
.AddSingleton<BotService>()
|
||||||
.AddSingleton<LlmService>();
|
.AddSingleton<LlmService>()
|
||||||
|
.AddHttpClient()
|
||||||
|
.AddSingleton<PalworldApiService>();
|
||||||
|
|
||||||
var serviceProvider = services.BuildServiceProvider();
|
var serviceProvider = services.BuildServiceProvider();
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ public class LlmService
|
|||||||
_httpClient = new HttpClient();
|
_httpClient = new HttpClient();
|
||||||
|
|
||||||
_endpoint = configuration["Llm:Endpoint"] ?? throw new InvalidOperationException("Llm:Endpoint is not configured");
|
_endpoint = configuration["Llm:Endpoint"] ?? throw new InvalidOperationException("Llm:Endpoint is not configured");
|
||||||
_apiKey = configuration["Llm:ApiKey"] ?? throw new InvalidOperationException("Llm:ApiKey is not configured");
|
_apiKey = Environment.GetEnvironmentVariable("LLM_API_KEY")
|
||||||
|
?? throw new InvalidOperationException("LLM_API_KEY environment variable is not set");
|
||||||
_model = configuration["Llm:Model"] ?? throw new InvalidOperationException("Llm:Model is not configured");
|
_model = configuration["Llm:Model"] ?? throw new InvalidOperationException("Llm:Model is not configured");
|
||||||
_maxTokens = int.Parse(configuration["Llm:MaxTokens"] ?? "1024");
|
_maxTokens = int.Parse(configuration["Llm:MaxTokens"] ?? "1024");
|
||||||
_temperature = double.Parse(configuration["Llm:Temperature"] ?? "0.7");
|
_temperature = double.Parse(configuration["Llm:Temperature"] ?? "0.7");
|
||||||
|
|||||||
84
Services/PalworldApiService.cs
Normal file
84
Services/PalworldApiService.cs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace TentacleBot.Services;
|
||||||
|
|
||||||
|
public class PalworldApiService
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly ILogger<PalworldApiService> _logger;
|
||||||
|
private readonly string _baseUrl;
|
||||||
|
|
||||||
|
public PalworldApiService(
|
||||||
|
IHttpClientFactory httpClientFactory,
|
||||||
|
IConfiguration configuration,
|
||||||
|
ILogger<PalworldApiService> logger)
|
||||||
|
{
|
||||||
|
_httpClient = httpClientFactory.CreateClient("PalworldApi");
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
_baseUrl = configuration["Palworld:BaseUrl"]
|
||||||
|
?? throw new InvalidOperationException("Palworld:BaseUrl not configured");
|
||||||
|
|
||||||
|
var username = configuration["Palworld:Username"] ?? "admin";
|
||||||
|
var password = Environment.GetEnvironmentVariable("PALWORLD_PASSWORD")
|
||||||
|
?? throw new InvalidOperationException("PALWORLD_PASSWORD environment variable is not set");
|
||||||
|
|
||||||
|
var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{username}:{password}"));
|
||||||
|
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials);
|
||||||
|
_httpClient.Timeout = TimeSpan.FromSeconds(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<JsonDocument?> GetInfoAsync()
|
||||||
|
{
|
||||||
|
return await GetAsync("info");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<JsonDocument?> GetPlayersAsync()
|
||||||
|
{
|
||||||
|
return await GetAsync("players");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<JsonDocument?> GetMetricsAsync()
|
||||||
|
{
|
||||||
|
return await GetAsync("metrics");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<JsonDocument?> GetAsync(string endpoint)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var url = $"{_baseUrl.TrimEnd('/')}/v1/api/{endpoint}";
|
||||||
|
var response = await _httpClient.GetAsync(url);
|
||||||
|
|
||||||
|
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
_logger.LogError("Palworld API returned 401 Unauthorized. Check your Palworld:Username and Palworld:Password config.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var json = await response.Content.ReadAsStringAsync();
|
||||||
|
return JsonDocument.Parse(json);
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
_logger.LogError("Palworld API request timed out at {Endpoint}", endpoint);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "HTTP error calling Palworld API {Endpoint}", endpoint);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (JsonException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to parse Palworld API response from {Endpoint}", endpoint);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
18
appsettings.json
Normal file
18
appsettings.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"Llm": {
|
||||||
|
"Endpoint": "http://compute.local:8001/v1/chat/completions",
|
||||||
|
"Model": "Qwen3.6-35B-A3B",
|
||||||
|
"MaxTokens": 100024,
|
||||||
|
"Temperature": 0.7
|
||||||
|
},
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Palworld": {
|
||||||
|
"BaseUrl": "http://compute.local:8212",
|
||||||
|
"Username": "admin"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
tentaclebot:
|
tentaclebot:
|
||||||
|
image: git.gpatti.com/gpatti/tentaclebot:latest
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
|||||||
Reference in New Issue
Block a user