feat: finalize operations model workspace

This commit is contained in:
Codex Local
2026-08-31 11:29:56 +08:00
parent 78e0a19acb
commit a8cdec98d1
15 changed files with 406 additions and 63 deletions
+2 -3
View File
@@ -334,11 +334,10 @@ func (s *Service) resolveActor(ctx context.Context, principal identity.Principal
var result actor
err := s.registry.pool.QueryRow(ctx, `
SELECT t.id::text,t.public_id::text,ua.id::text,ua.public_id::text,
COALESCE(bool_or(hm.role IN ('owner','admin') AND hm.status='active'),false)
ua.account_kind='operator' AND ua.platform_role IN ('ops_operator','ops_admin')
FROM tenant t JOIN user_account ua ON ua.tenant_id=t.id AND ua.status='active'
LEFT JOIN household_member hm ON hm.user_id=ua.id AND hm.tenant_id=t.id
WHERE t.public_id=$1::uuid AND t.status='active' AND ua.oidc_issuer=$2 AND ua.oidc_subject=$3
GROUP BY t.id,t.public_id,ua.id,ua.public_id`, tenantPublicID, principal.Issuer, principal.Subject).Scan(&result.tenantID, &result.tenantPublicID, &result.userID, &result.userPublicID, &result.canManage)
`, tenantPublicID, principal.Issuer, principal.Subject).Scan(&result.tenantID, &result.tenantPublicID, &result.userID, &result.userPublicID, &result.canManage)
if errors.Is(err, pgx.ErrNoRows) {
return actor{}, ErrAccessDenied
}
@@ -3,6 +3,7 @@ package contentmodel
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"os"
@@ -38,7 +39,14 @@ func TestServiceTestsAndActivatesCompatibleModel(t *testing.T) {
if err != nil {
t.Fatal(err)
}
principal := identity.Principal{Issuer: "http://localhost:58180/realms/voice-local", Subject: "00000000-0000-0000-0000-000000000101"}
principal := identity.Principal{Issuer: "http://localhost:58180/realms/voice-local", Subject: "00000000-0000-0000-0000-000000000104"}
customerPrincipal := identity.Principal{Issuer: "http://localhost:58180/realms/voice-local", Subject: "00000000-0000-0000-0000-000000000101"}
if _, err := service.Create(ctx, customerPrincipal, CreateInput{
TenantID: "10000000-0000-0000-0000-000000000010", DisplayName: "家庭端不应创建", Provider: "openai_compatible",
BaseURL: server.URL + "/v1", ModelID: "customer-model", APIKey: "integration-key", Temperature: 0.1, MaxTokens: 512,
}, "content-model-customer-create"); !errors.Is(err, ErrAccessDenied) {
t.Fatalf("customer content model create error=%v want access denied", err)
}
created, err := service.Create(ctx, principal, CreateInput{
TenantID: "10000000-0000-0000-0000-000000000010", DisplayName: "集成测试模型", Provider: "openai_compatible",
BaseURL: server.URL + "/v1", ModelID: "test-model", APIKey: "integration-key", Temperature: 0.1, MaxTokens: 512,
+4 -1
View File
@@ -91,7 +91,10 @@ func (p *Processor) Handle(ctx context.Context, event outbox.Event) error {
}
sourceTurns := subjectTurns(snapshot.turns)
if len(sourceTurns) == 0 {
return outbox.Retryable(ErrConversationContentNotReady)
// Terminal conversations cannot receive more subject turns. Treat the
// extraction request as complete so permanently empty conversations do
// not cycle through retry and dead-letter queues forever.
return nil
}
content := joinedConversationContent(snapshot.turns)
generated, err := p.generateSafely(ctx, content, snapshot.tenantID)
@@ -39,6 +39,7 @@ func TestProcessorExtractionIdempotencyAndSensitiveContent(t *testing.T) {
consentSnapshotID, consentSnapshotPublicID := uuid.NewString(), uuid.NewString()
contextID, contextPublicID := uuid.NewString(), uuid.NewString()
conversationID, conversationPublicID := uuid.NewString(), uuid.NewString()
emptyConversationID, emptyConversationPublicID := uuid.NewString(), uuid.NewString()
defer func() {
_, _ = pool.Exec(ctx, `DELETE FROM conversation_content_assessment WHERE conversation_id=$1::uuid`, conversationID)
_, _ = pool.Exec(ctx, `DELETE FROM fact_candidate WHERE story_id IN (SELECT id FROM story_draft WHERE conversation_id=$1::uuid)`, conversationID)
@@ -49,6 +50,7 @@ func TestProcessorExtractionIdempotencyAndSensitiveContent(t *testing.T) {
_, _ = pool.Exec(ctx, `DELETE FROM conversation_summary WHERE conversation_id=$1::uuid`, conversationID)
_, _ = pool.Exec(ctx, `DELETE FROM conversation_turn WHERE conversation_id=$1::uuid`, conversationID)
_, _ = pool.Exec(ctx, `DELETE FROM conversation WHERE id=$1::uuid`, conversationID)
_, _ = pool.Exec(ctx, `DELETE FROM conversation WHERE id=$1::uuid`, emptyConversationID)
_, _ = pool.Exec(ctx, `DELETE FROM context_snapshot WHERE id=$1::uuid`, contextID)
_, _ = pool.Exec(ctx, `DELETE FROM consent_snapshot WHERE id=$1::uuid`, consentSnapshotID)
}()
@@ -74,6 +76,18 @@ func TestProcessorExtractionIdempotencyAndSensitiveContent(t *testing.T) {
conversationID, conversationPublicID, consentSnapshotID, contextID, "extract-task-"+conversationID); err != nil {
t.Fatal(err)
}
if _, err := pool.Exec(ctx, `
INSERT INTO conversation(id,public_id,tenant_id,household_id,subject_id,device_id,consent_snapshot_id,context_snapshot_id,
provider_task_ref,engine_mode,retention_mode,status,allocation_expires_at,started_at,ended_at,stop_reason)
VALUES($1::uuid,$2::uuid,'10000000-0000-0000-0000-000000000001','10000000-0000-0000-0000-000000000201',
'10000000-0000-0000-0000-000000000401','10000000-0000-0000-0000-000000000501',
$3::uuid,$4::uuid,$5,'s2s_hybrid','transcript_only','COMPLETED',now()+interval '5 minutes',now()-interval '1 minute',now(),'user_hangup')`,
emptyConversationID, emptyConversationPublicID, consentSnapshotID, contextID, "extract-empty-task-"+emptyConversationID); err != nil {
t.Fatal(err)
}
if err := processor.Handle(ctx, outbox.Event{AggregateID: emptyConversationID, Payload: []byte(`{"generation_version":1}`)}); err != nil {
t.Fatalf("terminal conversation without subject turns should not retry: %v", err)
}
if _, err := pool.Exec(ctx, `
INSERT INTO conversation_turn(tenant_id,subject_id,conversation_id,turn_key,sequence_no,speaker,start_at,end_at,final_text,source_type)
VALUES('10000000-0000-0000-0000-000000000001','10000000-0000-0000-0000-000000000401',$1::uuid,$2,1,'subject',now()-interval '2 minutes',now()-interval '110 seconds',$3,'provider'),
-3
View File
@@ -27,7 +27,6 @@ const (
CapabilityRecordingDownload Capability = "recording.download"
CapabilityPrivacyRead Capability = "privacy.read"
CapabilityPrivacyManage Capability = "privacy.manage"
CapabilityModelManage Capability = "model.manage"
)
var roleCapabilities = map[Role]map[Capability]struct{}{
@@ -36,14 +35,12 @@ var roleCapabilities = map[Role]map[Capability]struct{}{
CapabilityOwnershipTransfer, CapabilitySubjectRead, CapabilitySubjectWrite,
CapabilityConversationRead, CapabilityStoryReview, CapabilityDeviceManage,
CapabilityFirmwareManage, CapabilityPrivacyRead, CapabilityPrivacyManage,
CapabilityModelManage,
),
RoleAdmin: capabilitySet(
CapabilityHouseholdRead, CapabilityHouseholdManage, CapabilityMemberManage,
CapabilitySubjectRead, CapabilitySubjectWrite, CapabilityConversationRead,
CapabilityStoryReview, CapabilityDeviceManage,
CapabilityFirmwareManage, CapabilityPrivacyRead,
CapabilityModelManage,
),
RoleEditor: capabilitySet(
CapabilityHouseholdRead, CapabilitySubjectRead, CapabilitySubjectWrite,