✨ Add billing
This commit is contained in:
21
DysonNetwork.Insight/Controllers/BillingController.cs
Normal file
21
DysonNetwork.Insight/Controllers/BillingController.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using DysonNetwork.Insight.Thought;
|
||||||
|
using DysonNetwork.Shared.Auth;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Insight.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("/api/billing")]
|
||||||
|
public class BillingController(ThoughtService thoughtService, ILogger<BillingController> logger) : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpPost("settle")]
|
||||||
|
[Authorize]
|
||||||
|
[RequiredPermission("maintenance", "insight.billing.settle")]
|
||||||
|
public async Task<IActionResult> ProcessTokenBilling()
|
||||||
|
{
|
||||||
|
await thoughtService.SettleThoughtBills(logger);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,9 @@
|
|||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
|
||||||
<PackageReference Include="Microsoft.SemanticKernel.Plugins.Web" Version="1.66.0-alpha" />
|
<PackageReference Include="Microsoft.SemanticKernel.Plugins.Web" Version="1.66.0-alpha" />
|
||||||
|
<PackageReference Include="Quartz" Version="3.15.0" />
|
||||||
|
<PackageReference Include="Quartz.AspNetCore" Version="3.15.0" />
|
||||||
|
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.15.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
146
DysonNetwork.Insight/Migrations/20251026134218_AddBilling.Designer.cs
generated
Normal file
146
DysonNetwork.Insight/Migrations/20251026134218_AddBilling.Designer.cs
generated
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using DysonNetwork.Insight;
|
||||||
|
using DysonNetwork.Shared.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using NodaTime;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Insight.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDatabase))]
|
||||||
|
[Migration("20251026134218_AddBilling")]
|
||||||
|
partial class AddBilling
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "9.0.10")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingSequence", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("account_id");
|
||||||
|
|
||||||
|
b.Property<Instant>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("deleted_at");
|
||||||
|
|
||||||
|
b.Property<long>("PaidToken")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("paid_token");
|
||||||
|
|
||||||
|
b.Property<string>("Topic")
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("character varying(4096)")
|
||||||
|
.HasColumnName("topic");
|
||||||
|
|
||||||
|
b.Property<long>("TotalToken")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("total_token");
|
||||||
|
|
||||||
|
b.Property<Instant>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_thinking_sequences");
|
||||||
|
|
||||||
|
b.ToTable("thinking_sequences", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingThought", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<List<SnThinkingChunk>>("Chunks")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("chunks");
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("content");
|
||||||
|
|
||||||
|
b.Property<Instant>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("deleted_at");
|
||||||
|
|
||||||
|
b.Property<List<SnCloudFileReferenceObject>>("Files")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("files");
|
||||||
|
|
||||||
|
b.Property<string>("ModelName")
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("character varying(4096)")
|
||||||
|
.HasColumnName("model_name");
|
||||||
|
|
||||||
|
b.Property<int>("Role")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("role");
|
||||||
|
|
||||||
|
b.Property<Guid>("SequenceId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("sequence_id");
|
||||||
|
|
||||||
|
b.Property<long>("TokenCount")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("token_count");
|
||||||
|
|
||||||
|
b.Property<Instant>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_thinking_thoughts");
|
||||||
|
|
||||||
|
b.HasIndex("SequenceId")
|
||||||
|
.HasDatabaseName("ix_thinking_thoughts_sequence_id");
|
||||||
|
|
||||||
|
b.ToTable("thinking_thoughts", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingThought", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DysonNetwork.Shared.Models.SnThinkingSequence", "Sequence")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SequenceId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_thinking_thoughts_thinking_sequences_sequence_id");
|
||||||
|
|
||||||
|
b.Navigation("Sequence");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
62
DysonNetwork.Insight/Migrations/20251026134218_AddBilling.cs
Normal file
62
DysonNetwork.Insight/Migrations/20251026134218_AddBilling.cs
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Insight.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddBilling : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "model_name",
|
||||||
|
table: "thinking_thoughts",
|
||||||
|
type: "character varying(4096)",
|
||||||
|
maxLength: 4096,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<long>(
|
||||||
|
name: "token_count",
|
||||||
|
table: "thinking_thoughts",
|
||||||
|
type: "bigint",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0L);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<long>(
|
||||||
|
name: "paid_token",
|
||||||
|
table: "thinking_sequences",
|
||||||
|
type: "bigint",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0L);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<long>(
|
||||||
|
name: "total_token",
|
||||||
|
table: "thinking_sequences",
|
||||||
|
type: "bigint",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "model_name",
|
||||||
|
table: "thinking_thoughts");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "token_count",
|
||||||
|
table: "thinking_thoughts");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "paid_token",
|
||||||
|
table: "thinking_sequences");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "total_token",
|
||||||
|
table: "thinking_sequences");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,11 +44,19 @@ namespace DysonNetwork.Insight.Migrations
|
|||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasColumnName("deleted_at");
|
.HasColumnName("deleted_at");
|
||||||
|
|
||||||
|
b.Property<long>("PaidToken")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("paid_token");
|
||||||
|
|
||||||
b.Property<string>("Topic")
|
b.Property<string>("Topic")
|
||||||
.HasMaxLength(4096)
|
.HasMaxLength(4096)
|
||||||
.HasColumnType("character varying(4096)")
|
.HasColumnType("character varying(4096)")
|
||||||
.HasColumnName("topic");
|
.HasColumnName("topic");
|
||||||
|
|
||||||
|
b.Property<long>("TotalToken")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("total_token");
|
||||||
|
|
||||||
b.Property<Instant>("UpdatedAt")
|
b.Property<Instant>("UpdatedAt")
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasColumnName("updated_at");
|
.HasColumnName("updated_at");
|
||||||
@@ -88,6 +96,11 @@ namespace DysonNetwork.Insight.Migrations
|
|||||||
.HasColumnType("jsonb")
|
.HasColumnType("jsonb")
|
||||||
.HasColumnName("files");
|
.HasColumnName("files");
|
||||||
|
|
||||||
|
b.Property<string>("ModelName")
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("character varying(4096)")
|
||||||
|
.HasColumnName("model_name");
|
||||||
|
|
||||||
b.Property<int>("Role")
|
b.Property<int>("Role")
|
||||||
.HasColumnType("integer")
|
.HasColumnType("integer")
|
||||||
.HasColumnName("role");
|
.HasColumnName("role");
|
||||||
@@ -96,6 +109,10 @@ namespace DysonNetwork.Insight.Migrations
|
|||||||
.HasColumnType("uuid")
|
.HasColumnType("uuid")
|
||||||
.HasColumnName("sequence_id");
|
.HasColumnName("sequence_id");
|
||||||
|
|
||||||
|
b.Property<long>("TokenCount")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("token_count");
|
||||||
|
|
||||||
b.Property<Instant>("UpdatedAt")
|
b.Property<Instant>("UpdatedAt")
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasColumnName("updated_at");
|
.HasColumnName("updated_at");
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ builder.Services.AddAppServices();
|
|||||||
builder.Services.AddAppAuthentication();
|
builder.Services.AddAppAuthentication();
|
||||||
builder.Services.AddAppFlushHandlers();
|
builder.Services.AddAppFlushHandlers();
|
||||||
builder.Services.AddAppBusinessServices();
|
builder.Services.AddAppBusinessServices();
|
||||||
|
builder.Services.AddAppScheduledJobs();
|
||||||
|
|
||||||
builder.Services.AddDysonAuth();
|
builder.Services.AddDysonAuth();
|
||||||
builder.Services.AddAccountService();
|
builder.Services.AddAccountService();
|
||||||
|
|||||||
25
DysonNetwork.Insight/Startup/ScheduledJobsConfiguration.cs
Normal file
25
DysonNetwork.Insight/Startup/ScheduledJobsConfiguration.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using Quartz;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Insight.Startup;
|
||||||
|
|
||||||
|
public static class ScheduledJobsConfiguration
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddAppScheduledJobs(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddQuartz(q =>
|
||||||
|
{
|
||||||
|
var tokenBillingJob = new JobKey("TokenBilling");
|
||||||
|
q.AddJob<TokenBillingJob>(opts => opts.WithIdentity(tokenBillingJob));
|
||||||
|
q.AddTrigger(opts => opts
|
||||||
|
.ForJob(tokenBillingJob)
|
||||||
|
.WithIdentity("TokenBillingTrigger")
|
||||||
|
.WithSimpleSchedule(o => o
|
||||||
|
.WithIntervalInMinutes(5)
|
||||||
|
.RepeatForever())
|
||||||
|
);
|
||||||
|
});
|
||||||
|
services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,6 +66,14 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddSingleton<ThoughtProvider>();
|
services.AddSingleton<ThoughtProvider>();
|
||||||
services.AddScoped<ThoughtService>();
|
services.AddScoped<ThoughtService>();
|
||||||
|
|
||||||
|
// Add gRPC clients for ThoughtService
|
||||||
|
services.AddGrpcClient<Shared.Proto.PaymentService.PaymentServiceClient>(o => o.Address = new Uri("https://_grpc.pass"))
|
||||||
|
.ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
|
||||||
|
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true });
|
||||||
|
services.AddGrpcClient<Shared.Proto.WalletService.WalletServiceClient>(o => o.Address = new Uri("https://_grpc.pass"))
|
||||||
|
.ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
|
||||||
|
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true });
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
DysonNetwork.Insight/Startup/TokenBillingJob.cs
Normal file
12
DysonNetwork.Insight/Startup/TokenBillingJob.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using DysonNetwork.Insight.Thought;
|
||||||
|
using Quartz;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Insight.Startup;
|
||||||
|
|
||||||
|
public class TokenBillingJob(ThoughtService thoughtService, ILogger<TokenBillingJob> logger) : IJob
|
||||||
|
{
|
||||||
|
public async Task Execute(IJobExecutionContext context)
|
||||||
|
{
|
||||||
|
await thoughtService.SettleThoughtBills(logger);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -178,7 +178,8 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service)
|
|||||||
sequence,
|
sequence,
|
||||||
accumulatedContent.ToString(),
|
accumulatedContent.ToString(),
|
||||||
ThinkingThoughtRole.Assistant,
|
ThinkingThoughtRole.Assistant,
|
||||||
thinkingChunks
|
thinkingChunks,
|
||||||
|
provider.ModelDefault
|
||||||
);
|
);
|
||||||
|
|
||||||
// Write the topic if it was newly set, then the thought object as JSON to the stream
|
// Write the topic if it was newly set, then the thought object as JSON to the stream
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public class ThoughtProvider
|
|||||||
public Kernel Kernel { get; }
|
public Kernel Kernel { get; }
|
||||||
|
|
||||||
private string? ModelProviderType { get; set; }
|
private string? ModelProviderType { get; set; }
|
||||||
private string? ModelDefault { get; set; }
|
public string? ModelDefault { get; set; }
|
||||||
|
|
||||||
[Experimental("SKEXP0050")]
|
[Experimental("SKEXP0050")]
|
||||||
public ThoughtProvider(
|
public ThoughtProvider(
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
using DysonNetwork.Shared.Cache;
|
using DysonNetwork.Shared.Cache;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
|
using DysonNetwork.Shared.Proto;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using PaymentService = DysonNetwork.Shared.Proto.PaymentService;
|
||||||
|
using TransactionType = DysonNetwork.Shared.Proto.TransactionType;
|
||||||
|
using WalletService = DysonNetwork.Shared.Proto.WalletService;
|
||||||
|
|
||||||
namespace DysonNetwork.Insight.Thought;
|
namespace DysonNetwork.Insight.Thought;
|
||||||
|
|
||||||
public class ThoughtService(AppDatabase db, ICacheService cache)
|
public class ThoughtService(AppDatabase db, ICacheService cache, PaymentService.PaymentServiceClient paymentService, WalletService.WalletServiceClient walletService)
|
||||||
{
|
{
|
||||||
public async Task<SnThinkingSequence?> GetOrCreateSequenceAsync(Guid accountId, Guid? sequenceId,
|
public async Task<SnThinkingSequence?> GetOrCreateSequenceAsync(Guid accountId, Guid? sequenceId,
|
||||||
string? topic = null)
|
string? topic = null)
|
||||||
@@ -28,17 +32,28 @@ public class ThoughtService(AppDatabase db, ICacheService cache)
|
|||||||
SnThinkingSequence sequence,
|
SnThinkingSequence sequence,
|
||||||
string content,
|
string content,
|
||||||
ThinkingThoughtRole role,
|
ThinkingThoughtRole role,
|
||||||
List<SnThinkingChunk>? chunks = null
|
List<SnThinkingChunk>? chunks = null,
|
||||||
|
string? model = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
// Approximate token count (1 token ≈ 4 characters for GPT-like models)
|
||||||
|
var tokenCount = content?.Length / 4 ?? 0;
|
||||||
|
|
||||||
var thought = new SnThinkingThought
|
var thought = new SnThinkingThought
|
||||||
{
|
{
|
||||||
SequenceId = sequence.Id,
|
SequenceId = sequence.Id,
|
||||||
Content = content,
|
Content = content,
|
||||||
Role = role,
|
Role = role,
|
||||||
Chunks = chunks ?? []
|
TokenCount = tokenCount,
|
||||||
|
ModelName = model,
|
||||||
|
Chunks = chunks ?? new List<SnThinkingChunk>()
|
||||||
};
|
};
|
||||||
db.ThinkingThoughts.Add(thought);
|
db.ThinkingThoughts.Add(thought);
|
||||||
|
|
||||||
|
// Update sequence total tokens only for assistant responses
|
||||||
|
if (role == ThinkingThoughtRole.Assistant)
|
||||||
|
sequence.TotalToken += tokenCount;
|
||||||
|
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
// Invalidate cache for this sequence's thoughts
|
// Invalidate cache for this sequence's thoughts
|
||||||
@@ -80,4 +95,58 @@ public class ThoughtService(AppDatabase db, ICacheService cache)
|
|||||||
|
|
||||||
return (totalCount, sequences);
|
return (totalCount, sequences);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public async Task SettleThoughtBills(ILogger logger)
|
||||||
|
{
|
||||||
|
var sequences = await db.ThinkingSequences
|
||||||
|
.Where(s => s.PaidToken < s.TotalToken)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (sequences.Count == 0)
|
||||||
|
{
|
||||||
|
logger.LogInformation("No unpaid sequences found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group by account
|
||||||
|
var groupedByAccount = sequences.GroupBy(s => s.AccountId);
|
||||||
|
|
||||||
|
foreach (var accountGroup in groupedByAccount)
|
||||||
|
{
|
||||||
|
var accountId = accountGroup.Key;
|
||||||
|
var totalUnpaidTokens = accountGroup.Sum(s => s.TotalToken - s.PaidToken);
|
||||||
|
var cost = (long)Math.Ceiling(totalUnpaidTokens / 1000.0);
|
||||||
|
|
||||||
|
if (cost == 0) continue;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var walletResponse = await walletService.GetWalletAsync(new GetWalletRequest { AccountId = accountId.ToString() });
|
||||||
|
var walletId = Guid.Parse(walletResponse.Id);
|
||||||
|
|
||||||
|
var date = DateTime.Now.ToString("yyyy-MM-dd");
|
||||||
|
await paymentService.CreateTransactionAsync(new CreateTransactionRequest
|
||||||
|
{
|
||||||
|
PayerWalletId = walletId.ToString(),
|
||||||
|
PayeeWalletId = null,
|
||||||
|
Currency = WalletCurrency.SourcePoint,
|
||||||
|
Amount = cost.ToString(),
|
||||||
|
Remarks = $"Wage for SN-chan on {date}",
|
||||||
|
Type = TransactionType.System
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mark all sequences for this account as paid
|
||||||
|
foreach (var sequence in accountGroup)
|
||||||
|
sequence.PaidToken = sequence.TotalToken;
|
||||||
|
|
||||||
|
logger.LogInformation("Billed {cost} points for account {accountId}", cost, accountId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error billing for account {accountId}", accountId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,6 @@
|
|||||||
"Thinking": {
|
"Thinking": {
|
||||||
"Provider": "deepseek",
|
"Provider": "deepseek",
|
||||||
"Model": "deepseek-chat",
|
"Model": "deepseek-chat",
|
||||||
"ApiKey": "sk-cd709f9f1b96432e99d2d992392b4220"
|
"ApiKey": "sk-bd20f6a2e9fa40b98c46899baa0e9f09"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,7 @@ using NodaTime.Serialization.Protobuf;
|
|||||||
|
|
||||||
namespace DysonNetwork.Shared.Models;
|
namespace DysonNetwork.Shared.Models;
|
||||||
|
|
||||||
public class WalletCurrency
|
public abstract class WalletCurrency
|
||||||
{
|
{
|
||||||
public const string SourcePoint = "points";
|
public const string SourcePoint = "points";
|
||||||
public const string GoldenPoint = "golds";
|
public const string GoldenPoint = "golds";
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ public class SnThinkingSequence : ModelBase
|
|||||||
{
|
{
|
||||||
public Guid Id { get; set; } = Guid.NewGuid();
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
[MaxLength(4096)] public string? Topic { get; set; }
|
[MaxLength(4096)] public string? Topic { get; set; }
|
||||||
|
|
||||||
|
public long TotalToken { get; set; }
|
||||||
|
public long PaidToken { get; set; }
|
||||||
|
|
||||||
public Guid AccountId { get; set; }
|
public Guid AccountId { get; set; }
|
||||||
}
|
}
|
||||||
@@ -38,11 +41,13 @@ public class SnThinkingThought : ModelBase
|
|||||||
public string? Content { get; set; }
|
public string? Content { get; set; }
|
||||||
|
|
||||||
[Column(TypeName = "jsonb")] public List<SnCloudFileReferenceObject> Files { get; set; } = [];
|
[Column(TypeName = "jsonb")] public List<SnCloudFileReferenceObject> Files { get; set; } = [];
|
||||||
|
|
||||||
[Column(TypeName = "jsonb")] public List<SnThinkingChunk> Chunks { get; set; } = [];
|
[Column(TypeName = "jsonb")] public List<SnThinkingChunk> Chunks { get; set; } = [];
|
||||||
|
|
||||||
public ThinkingThoughtRole Role { get; set; }
|
public ThinkingThoughtRole Role { get; set; }
|
||||||
|
|
||||||
|
public long TokenCount { get; set; }
|
||||||
|
[MaxLength(4096)] public string? ModelName { get; set; }
|
||||||
|
|
||||||
public Guid SequenceId { get; set; }
|
public Guid SequenceId { get; set; }
|
||||||
[JsonIgnore] public SnThinkingSequence Sequence { get; set; } = null!;
|
[JsonIgnore] public SnThinkingSequence Sequence { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,11 @@ public static class ServiceInjectionHelper
|
|||||||
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true }
|
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
services.AddGrpcClient<WalletService.WalletServiceClient>(o => o.Address = new Uri("https://_grpc.pass"))
|
||||||
|
.ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
|
||||||
|
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true }
|
||||||
|
);
|
||||||
|
|
||||||
services
|
services
|
||||||
.AddGrpcClient<RealmService.RealmServiceClient>(o => o.Address = new Uri("https://_grpc.pass"))
|
.AddGrpcClient<RealmService.RealmServiceClient>(o => o.Address = new Uri("https://_grpc.pass"))
|
||||||
.ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
|
.ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
|
||||||
@@ -113,4 +118,4 @@ public static class ServiceInjectionHelper
|
|||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user