You've already forked agentic-coding-workflow
feat(chat): migration 0004 + errs codes for chat/llm/storage
This commit is contained in:
@@ -44,6 +44,28 @@ const (
|
|||||||
CodeWorktreePathInvalid Code = "worktree_path_invalid"
|
CodeWorktreePathInvalid Code = "worktree_path_invalid"
|
||||||
CodeGitCredentialInvalid Code = "git_credential_invalid"
|
CodeGitCredentialInvalid Code = "git_credential_invalid"
|
||||||
CodeGitCmdFailed Code = "git_cmd_failed"
|
CodeGitCmdFailed Code = "git_cmd_failed"
|
||||||
|
|
||||||
|
// Chat 模块
|
||||||
|
CodeChatConversationNotFound Code = "chat.conversation_not_found"
|
||||||
|
CodeChatMessageNotFound Code = "chat.message_not_found"
|
||||||
|
CodeChatMessageNotPending Code = "chat.message_not_pending"
|
||||||
|
CodeChatConcurrentMessage Code = "chat.concurrent_message"
|
||||||
|
CodeChatAttachmentTooLarge Code = "chat.attachment_too_large"
|
||||||
|
CodeChatAttachmentUnsupportedMime Code = "chat.attachment_unsupported_mime"
|
||||||
|
CodeChatModelCapabilityMismatch Code = "chat.model_capability_mismatch"
|
||||||
|
CodeChatTemplateNotFound Code = "chat.template_not_found"
|
||||||
|
CodeChatEndpointNotFound Code = "chat.endpoint_not_found"
|
||||||
|
CodeChatModelNotFound Code = "chat.model_not_found"
|
||||||
|
|
||||||
|
// LLM 域
|
||||||
|
CodeLLMUpstream Code = "llm.upstream_error"
|
||||||
|
CodeLLMTimeout Code = "llm.timeout"
|
||||||
|
CodeLLMRateLimited Code = "llm.rate_limited"
|
||||||
|
CodeLLMContextTooLong Code = "llm.context_too_long"
|
||||||
|
|
||||||
|
// Storage 域
|
||||||
|
CodeStoragePutFailed Code = "storage.put_failed"
|
||||||
|
CodeStorageGetFailed Code = "storage.get_failed"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AppError is the application's structured error type. It carries a stable
|
// AppError is the application's structured error type. It carries a stable
|
||||||
@@ -131,6 +153,24 @@ func HTTPStatus(code Code) int {
|
|||||||
return http.StatusBadRequest
|
return http.StatusBadRequest
|
||||||
case CodeWorktreeNotHeldByCaller:
|
case CodeWorktreeNotHeldByCaller:
|
||||||
return http.StatusForbidden
|
return http.StatusForbidden
|
||||||
|
case CodeChatConversationNotFound, CodeChatMessageNotFound,
|
||||||
|
CodeChatTemplateNotFound, CodeChatEndpointNotFound, CodeChatModelNotFound:
|
||||||
|
return http.StatusNotFound
|
||||||
|
case CodeChatMessageNotPending, CodeChatConcurrentMessage,
|
||||||
|
CodeChatModelCapabilityMismatch:
|
||||||
|
return http.StatusConflict
|
||||||
|
case CodeChatAttachmentTooLarge:
|
||||||
|
return http.StatusRequestEntityTooLarge
|
||||||
|
case CodeChatAttachmentUnsupportedMime:
|
||||||
|
return http.StatusUnsupportedMediaType
|
||||||
|
case CodeLLMRateLimited:
|
||||||
|
return http.StatusTooManyRequests
|
||||||
|
case CodeLLMTimeout:
|
||||||
|
return http.StatusGatewayTimeout
|
||||||
|
case CodeLLMUpstream, CodeStoragePutFailed, CodeStorageGetFailed:
|
||||||
|
return http.StatusBadGateway
|
||||||
|
case CodeLLMContextTooLong:
|
||||||
|
return http.StatusBadRequest
|
||||||
default:
|
default:
|
||||||
return http.StatusInternalServerError
|
return http.StatusInternalServerError
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,25 @@ func TestHTTPStatus(t *testing.T) {
|
|||||||
CodeWorktreePathInvalid: http.StatusBadRequest,
|
CodeWorktreePathInvalid: http.StatusBadRequest,
|
||||||
CodeGitCredentialInvalid: http.StatusBadRequest,
|
CodeGitCredentialInvalid: http.StatusBadRequest,
|
||||||
CodeWorktreeNotHeldByCaller: http.StatusForbidden,
|
CodeWorktreeNotHeldByCaller: http.StatusForbidden,
|
||||||
|
// Chat codes
|
||||||
|
CodeChatConversationNotFound: http.StatusNotFound,
|
||||||
|
CodeChatMessageNotFound: http.StatusNotFound,
|
||||||
|
CodeChatMessageNotPending: http.StatusConflict,
|
||||||
|
CodeChatConcurrentMessage: http.StatusConflict,
|
||||||
|
CodeChatAttachmentTooLarge: http.StatusRequestEntityTooLarge,
|
||||||
|
CodeChatAttachmentUnsupportedMime: http.StatusUnsupportedMediaType,
|
||||||
|
CodeChatModelCapabilityMismatch: http.StatusConflict,
|
||||||
|
CodeChatTemplateNotFound: http.StatusNotFound,
|
||||||
|
CodeChatEndpointNotFound: http.StatusNotFound,
|
||||||
|
CodeChatModelNotFound: http.StatusNotFound,
|
||||||
|
// LLM codes
|
||||||
|
CodeLLMUpstream: http.StatusBadGateway,
|
||||||
|
CodeLLMTimeout: http.StatusGatewayTimeout,
|
||||||
|
CodeLLMRateLimited: http.StatusTooManyRequests,
|
||||||
|
CodeLLMContextTooLong: http.StatusBadRequest,
|
||||||
|
// Storage codes
|
||||||
|
CodeStoragePutFailed: http.StatusBadGateway,
|
||||||
|
CodeStorageGetFailed: http.StatusBadGateway,
|
||||||
}
|
}
|
||||||
for code, want := range cases {
|
for code, want := range cases {
|
||||||
require.Equal(t, want, HTTPStatus(code), "code=%s", code)
|
require.Equal(t, want, HTTPStatus(code), "code=%s", code)
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
DROP TABLE IF EXISTS llm_usage;
|
||||||
|
DROP TABLE IF EXISTS message_attachments;
|
||||||
|
DROP TABLE IF EXISTS messages;
|
||||||
|
DROP TABLE IF EXISTS conversations;
|
||||||
|
DROP TABLE IF EXISTS prompt_templates;
|
||||||
|
DROP TABLE IF EXISTS llm_models;
|
||||||
|
DROP TABLE IF EXISTS llm_endpoints;
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
-- ============ LLM Endpoints ============
|
||||||
|
CREATE TABLE llm_endpoints (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
provider TEXT NOT NULL CHECK (provider IN ('anthropic','openai','gemini')),
|
||||||
|
display_name TEXT NOT NULL,
|
||||||
|
base_url TEXT NOT NULL,
|
||||||
|
api_key_encrypted BYTEA NOT NULL,
|
||||||
|
created_by UUID NOT NULL REFERENCES users(id),
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
CREATE INDEX llm_endpoints_provider_idx ON llm_endpoints(provider);
|
||||||
|
|
||||||
|
-- ============ LLM Models ============
|
||||||
|
CREATE TABLE llm_models (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
endpoint_id UUID NOT NULL REFERENCES llm_endpoints(id) ON DELETE RESTRICT,
|
||||||
|
model_id TEXT NOT NULL,
|
||||||
|
display_name TEXT NOT NULL,
|
||||||
|
capabilities JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||||
|
context_window INT NOT NULL,
|
||||||
|
max_output_tokens INT NOT NULL,
|
||||||
|
prompt_price_per_million_usd NUMERIC(10,6) NOT NULL DEFAULT 0,
|
||||||
|
completion_price_per_million_usd NUMERIC(10,6) NOT NULL DEFAULT 0,
|
||||||
|
thinking_price_per_million_usd NUMERIC(10,6) NOT NULL DEFAULT 0,
|
||||||
|
is_title_generator BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
enabled BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
sort_order INT NOT NULL DEFAULT 0,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
UNIQUE (endpoint_id, model_id)
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX llm_models_one_title_generator
|
||||||
|
ON llm_models ((1)) WHERE is_title_generator;
|
||||||
|
CREATE INDEX llm_models_enabled_sort_idx ON llm_models(enabled, sort_order);
|
||||||
|
|
||||||
|
-- ============ Prompt Templates ============
|
||||||
|
CREATE TABLE prompt_templates (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
scope TEXT NOT NULL CHECK (scope IN ('system','user')),
|
||||||
|
owner_id UUID REFERENCES users(id),
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
CHECK ((scope='user' AND owner_id IS NOT NULL) OR (scope='system' AND owner_id IS NULL))
|
||||||
|
);
|
||||||
|
CREATE INDEX prompt_templates_scope_owner_idx ON prompt_templates(scope, owner_id);
|
||||||
|
|
||||||
|
-- ============ Conversations ============
|
||||||
|
CREATE TABLE conversations (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id),
|
||||||
|
project_id UUID REFERENCES projects(id),
|
||||||
|
workspace_id UUID REFERENCES workspaces(id),
|
||||||
|
issue_id UUID REFERENCES issues(id),
|
||||||
|
model_id UUID NOT NULL REFERENCES llm_models(id) ON DELETE RESTRICT,
|
||||||
|
system_prompt TEXT NOT NULL DEFAULT '',
|
||||||
|
title TEXT,
|
||||||
|
title_generated_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
CREATE INDEX conversations_user_list_idx ON conversations(user_id, deleted_at, updated_at DESC);
|
||||||
|
CREATE INDEX conversations_project_idx ON conversations(project_id) WHERE project_id IS NOT NULL;
|
||||||
|
CREATE INDEX conversations_workspace_idx ON conversations(workspace_id) WHERE workspace_id IS NOT NULL;
|
||||||
|
CREATE INDEX conversations_issue_idx ON conversations(issue_id) WHERE issue_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- ============ Messages ============
|
||||||
|
CREATE TABLE messages (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
||||||
|
role TEXT NOT NULL CHECK (role IN ('user','assistant')),
|
||||||
|
content TEXT NOT NULL DEFAULT '',
|
||||||
|
thinking TEXT,
|
||||||
|
tool_calls JSONB,
|
||||||
|
status TEXT NOT NULL CHECK (status IN ('pending','ok','error','cancelled')),
|
||||||
|
error_message TEXT,
|
||||||
|
prompt_tokens INT,
|
||||||
|
completion_tokens INT,
|
||||||
|
thinking_tokens INT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
CREATE INDEX messages_conv_id_idx ON messages(conversation_id, id);
|
||||||
|
CREATE UNIQUE INDEX messages_one_pending_per_conv
|
||||||
|
ON messages(conversation_id) WHERE status='pending';
|
||||||
|
|
||||||
|
-- ============ Message Attachments ============
|
||||||
|
CREATE TABLE message_attachments (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id),
|
||||||
|
message_id BIGINT REFERENCES messages(id) ON DELETE CASCADE,
|
||||||
|
filename TEXT NOT NULL,
|
||||||
|
mime_type TEXT NOT NULL,
|
||||||
|
size_bytes BIGINT NOT NULL,
|
||||||
|
sha256 TEXT NOT NULL,
|
||||||
|
storage_path TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
CREATE INDEX message_attachments_orphan_idx
|
||||||
|
ON message_attachments(user_id, created_at) WHERE message_id IS NULL;
|
||||||
|
CREATE INDEX message_attachments_msg_idx
|
||||||
|
ON message_attachments(message_id) WHERE message_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- ============ LLM Usage ============
|
||||||
|
CREATE TABLE llm_usage (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
conversation_id UUID NOT NULL,
|
||||||
|
message_id BIGINT NOT NULL,
|
||||||
|
user_id UUID NOT NULL,
|
||||||
|
endpoint_id UUID NOT NULL,
|
||||||
|
model_id UUID NOT NULL,
|
||||||
|
prompt_tokens INT NOT NULL,
|
||||||
|
completion_tokens INT NOT NULL,
|
||||||
|
thinking_tokens INT NOT NULL DEFAULT 0,
|
||||||
|
cost_usd NUMERIC(10,6) NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
CREATE INDEX llm_usage_user_time_idx ON llm_usage(user_id, created_at);
|
||||||
|
CREATE INDEX llm_usage_model_time_idx ON llm_usage(model_id, created_at);
|
||||||
|
CREATE INDEX llm_usage_time_idx ON llm_usage(created_at);
|
||||||
Reference in New Issue
Block a user