Files
whale-town-front-v2/scenes/ui/CafeCompanionRecruitmentPanel.gd

440 lines
16 KiB
GDScript

extends Control
# ============================================================================
# CafeCompanionRecruitmentPanel.gd - 咖啡店陪伴机器人招聘登记面板
# ============================================================================
# 点击咖啡馆吧台鲸鱼标识后打开,玩家提交陪伴 Agent 代理配置,
# 后端登记为咖啡店陪伴服务点上的被雇佣角色。
# ============================================================================
@onready var recruitmentFrame: PanelContainer = %CafeRecruitmentFrame
@onready var titleLabel: Label = %TitleLabel
@onready var subtitleLabel: Label = %SubtitleLabel
@onready var closeButton: Button = %CloseButton
@onready var servicePointOption: OptionButton = %ServicePointOption
@onready var employmentDurationOption: OptionButton = %EmploymentDurationOption
@onready var personaNameInput: LineEdit = %PersonaNameInput
@onready var protocolOption: OptionButton = %ProtocolOption
@onready var baseUrlLabel: Label = %BaseUrlLabel
@onready var baseUrlInput: LineEdit = %BaseUrlInput
@onready var tokenInput: LineEdit = %TokenInput
@onready var modelInput: LineEdit = %ModelInput
@onready var fetchModelsButton: Button = %FetchModelsButton
@onready var modelOption: OptionButton = %ModelOption
@onready var personaPromptInput: TextEdit = %PersonaPromptInput
@onready var welcomeMessageInput: LineEdit = %WelcomeMessageInput
@onready var enabledCheckBox: CheckBox = %EnabledCheckBox
@onready var submitButton: Button = %SubmitButton
@onready var statusLabel: Label = %StatusLabel
var _servicePoints: Array = []
var _isSubmitting: bool = false
var _isFetchingModels: bool = false
func _ready() -> void:
recruitmentFrame.visible = false
_setup_protocol_options()
_setup_employment_duration_options()
closeButton.pressed.connect(_on_close_pressed)
submitButton.pressed.connect(_on_submit_pressed)
fetchModelsButton.pressed.connect(_on_fetch_models_pressed)
protocolOption.item_selected.connect(_on_protocol_selected)
modelOption.item_selected.connect(_on_model_option_selected)
_connect_events()
func _exit_tree() -> void:
var eventSystem := get_node_or_null("/root/EventSystem")
if eventSystem == null:
return
eventSystem.call("disconnect_event", EventNames.CAFE_COMPANION_RECRUITMENT_SELECTED, _on_recruitment_selected, self)
eventSystem.call("disconnect_event", EventNames.CAFE_COMPANION_SERVICE_POINTS_READY, _on_service_points_ready, self)
eventSystem.call("disconnect_event", EventNames.CAFE_COMPANION_AGENT_REGISTERED, _on_agent_registered, self)
eventSystem.call("disconnect_event", EventNames.CAFE_COMPANION_MODELS_READY, _on_models_ready, self)
eventSystem.call("disconnect_event", EventNames.CAFE_COMPANION_ERROR_OCCURRED, _on_companion_error, self)
func _connect_events() -> void:
var eventSystem := get_node_or_null("/root/EventSystem")
if eventSystem == null:
push_warning("CafeCompanionRecruitmentPanel: EventSystem autoload is not available.")
return
eventSystem.call("connect_event", EventNames.CAFE_COMPANION_RECRUITMENT_SELECTED, _on_recruitment_selected, self)
eventSystem.call("connect_event", EventNames.CAFE_COMPANION_SERVICE_POINTS_READY, _on_service_points_ready, self)
eventSystem.call("connect_event", EventNames.CAFE_COMPANION_AGENT_REGISTERED, _on_agent_registered, self)
eventSystem.call("connect_event", EventNames.CAFE_COMPANION_MODELS_READY, _on_models_ready, self)
eventSystem.call("connect_event", EventNames.CAFE_COMPANION_ERROR_OCCURRED, _on_companion_error, self)
func _on_recruitment_selected(data: Dictionary) -> void:
titleLabel.text = str(data.get("title", "咖啡店陪伴招聘"))
subtitleLabel.text = "鲸鱼咖啡馆 · 吧台招聘终端"
recruitmentFrame.visible = true
_isSubmitting = false
_isFetchingModels = false
submitButton.disabled = false
fetchModelsButton.disabled = false
_apply_protocol_defaults(true)
_set_status("正在读取陪伴位...", false)
_request_service_points()
func _request_service_points() -> void:
var manager := _get_cafe_companion_manager()
if manager == null or not manager.has_method("get_service_points"):
_set_status("CafeCompanionManager 未加载", true)
return
manager.call("get_service_points", true)
func _on_service_points_ready(data: Dictionary) -> void:
if not recruitmentFrame.visible:
return
var servicePointsVariant: Variant = data.get("service_points", [])
_servicePoints = servicePointsVariant if servicePointsVariant is Array else []
_render_service_points()
func _on_agent_registered(data: Dictionary) -> void:
if not recruitmentFrame.visible:
return
_isSubmitting = false
submitButton.disabled = false
fetchModelsButton.disabled = false
tokenInput.clear()
var companionVariant: Variant = data.get("companion", {})
if companionVariant is Dictionary:
var companion: Dictionary = companionVariant
_set_status("%s 已登记到 %s" % [
str(companion.get("persona_name", "陪伴机器人")),
_format_service_point_label(str(companion.get("service_point_id", ""))),
], false)
else:
_set_status("咖啡店陪伴机器人登记成功", false)
recruitmentFrame.visible = false
release_focus()
func _on_companion_error(data: Dictionary) -> void:
if not recruitmentFrame.visible:
return
_isSubmitting = false
_isFetchingModels = false
submitButton.disabled = false
fetchModelsButton.disabled = false
_set_status(str(data.get("message", "咖啡店招聘暂时不可用")), true)
func _render_service_points() -> void:
servicePointOption.clear()
var selectedIndex := -1
for pointVariant in _servicePoints:
if not (pointVariant is Dictionary):
continue
var point: Dictionary = pointVariant
var pointId := str(point.get("id", "")).strip_edges()
if pointId.is_empty():
continue
var index := servicePointOption.get_item_count()
servicePointOption.add_item(_format_service_point_option(point))
servicePointOption.set_item_metadata(index, pointId)
if selectedIndex < 0 and not _point_has_companion(point):
selectedIndex = index
if servicePointOption.get_item_count() <= 0:
_set_status("当前没有开放的咖啡店陪伴位", true)
submitButton.disabled = true
return
servicePointOption.select(selectedIndex if selectedIndex >= 0 else 0)
submitButton.disabled = _isSubmitting
fetchModelsButton.disabled = _isSubmitting
_set_status("填写人设和代理配置后可登记", false)
func _format_service_point_option(point: Dictionary) -> String:
var pointId := str(point.get("id", "")).strip_edges()
var label := _format_service_point_label(pointId)
var companionVariant: Variant = point.get("companion", null)
if companionVariant is Dictionary:
var companion: Dictionary = companionVariant
var personaName := str(companion.get("persona_name", "")).strip_edges()
if not personaName.is_empty():
return "%s · 当前:%s" % [label, personaName]
return "%s · 空位" % label
func _point_has_companion(point: Dictionary) -> bool:
var companionVariant: Variant = point.get("companion", null)
return companionVariant is Dictionary and not (companionVariant as Dictionary).is_empty()
func _on_submit_pressed() -> void:
if _isSubmitting:
return
var payload := _build_payload()
if payload.is_empty():
return
var manager := _get_cafe_companion_manager()
if manager == null or not manager.has_method("register_employment_agent"):
_set_status("CafeCompanionManager 未加载", true)
return
_isSubmitting = true
submitButton.disabled = true
fetchModelsButton.disabled = true
_set_status("正在验证代理并提交雇佣登记...", false)
manager.call("register_employment_agent", payload)
func _on_fetch_models_pressed() -> void:
if _isFetchingModels or _isSubmitting:
return
var protocol := _selected_protocol()
var baseUrl := baseUrlInput.text.strip_edges()
var token := tokenInput.text.strip_edges()
if not (baseUrl.begins_with("http://") or baseUrl.begins_with("https://")):
_set_status("接口 URL 需要以 http:// 或 https:// 开头", true)
baseUrlInput.grab_focus()
return
if token.is_empty():
_set_status("请先填写接口 Token", true)
tokenInput.grab_focus()
return
var manager := _get_cafe_companion_manager()
if manager == null or not manager.has_method("get_employment_models"):
_set_status("CafeCompanionManager 未加载", true)
return
_isFetchingModels = true
fetchModelsButton.disabled = true
_set_status("正在获取接口可用模型...", false)
manager.call("get_employment_models", protocol, baseUrl, token)
func _on_models_ready(data: Dictionary) -> void:
if not recruitmentFrame.visible:
return
_isFetchingModels = false
fetchModelsButton.disabled = _isSubmitting
var modelsVariant: Variant = data.get("models", [])
var models: Array = modelsVariant if modelsVariant is Array else []
_render_models(models)
func _build_payload() -> Dictionary:
var servicePointId := _selected_service_point_id()
var personaName := personaNameInput.text.strip_edges()
var employmentMinutes := _selected_employment_minutes()
var protocol := _selected_protocol()
var baseUrl := baseUrlInput.text.strip_edges()
var token := tokenInput.text.strip_edges()
var model := _selected_model_id()
var personaPrompt := personaPromptInput.text.strip_edges()
var welcomeMessage := welcomeMessageInput.text.strip_edges()
if servicePointId.is_empty():
_set_status("请选择陪伴位", true)
return {}
if personaName.is_empty():
_set_status("请填写人设名称", true)
personaNameInput.grab_focus()
return {}
if employmentMinutes <= 0:
_set_status("请选择打工时间", true)
return {}
if not (baseUrl.begins_with("http://") or baseUrl.begins_with("https://")):
_set_status("接口 URL 需要以 http:// 或 https:// 开头", true)
baseUrlInput.grab_focus()
return {}
if token.is_empty():
_set_status("请填写接口 Token", true)
tokenInput.grab_focus()
return {}
if model.is_empty():
_set_status("请填写模型名称", true)
modelInput.grab_focus()
return {}
if personaPrompt.is_empty():
_set_status("请填写人设指令", true)
personaPromptInput.grab_focus()
return {}
var payload := {
"service_point_id": servicePointId,
"persona_name": personaName,
"employment_minutes": employmentMinutes,
"protocol": protocol,
"base_url": baseUrl,
"token": token,
"model": model,
"persona_prompt": personaPrompt,
"enabled": enabledCheckBox.button_pressed,
}
if not welcomeMessage.is_empty():
payload["welcome_message"] = welcomeMessage
return payload
func _setup_employment_duration_options() -> void:
employmentDurationOption.clear()
var durations := [
{"label": "30分钟", "minutes": 30},
{"label": "1小时", "minutes": 60},
{"label": "2小时", "minutes": 120},
{"label": "4小时", "minutes": 240},
{"label": "8小时", "minutes": 480},
]
for duration in durations:
var index := employmentDurationOption.get_item_count()
employmentDurationOption.add_item(str(duration.get("label", "")))
employmentDurationOption.set_item_metadata(index, int(duration.get("minutes", 0)))
employmentDurationOption.select(1)
func _selected_employment_minutes() -> int:
var selectedIndex := employmentDurationOption.selected
if selectedIndex < 0:
return 0
return int(employmentDurationOption.get_item_metadata(selectedIndex))
func _selected_service_point_id() -> String:
var selectedIndex := servicePointOption.selected
if selectedIndex < 0:
return ""
return str(servicePointOption.get_item_metadata(selectedIndex)).strip_edges()
func _setup_protocol_options() -> void:
protocolOption.clear()
protocolOption.add_item("OpenAI", 0)
protocolOption.set_item_metadata(0, "openai")
protocolOption.add_item("Anthropic", 1)
protocolOption.set_item_metadata(1, "anthropic")
protocolOption.select(0)
_apply_protocol_defaults(true)
func _on_protocol_selected(_index: int) -> void:
_apply_protocol_defaults(true)
_set_status("%s 协议已选择,可填写接口并获取模型" % _selected_protocol_label(), false)
func _selected_protocol() -> String:
var selectedIndex := protocolOption.selected
if selectedIndex < 0:
return "openai"
var protocol := str(protocolOption.get_item_metadata(selectedIndex)).strip_edges()
return protocol if not protocol.is_empty() else "openai"
func _selected_protocol_label() -> String:
return "Anthropic" if _selected_protocol() == "anthropic" else "OpenAI"
func _apply_protocol_defaults(clearModelList: bool) -> void:
var protocol := _selected_protocol()
if protocol == "anthropic":
baseUrlLabel.text = "Anthropic URL"
baseUrlInput.placeholder_text = "https://api.anthropic.com"
modelInput.placeholder_text = "claude-sonnet-4-5-20250929"
if modelInput.text.strip_edges().is_empty() or modelInput.text.strip_edges() == "gpt-4o-mini":
modelInput.text = "claude-sonnet-4-5-20250929"
else:
baseUrlLabel.text = "OpenAI-compatible URL"
baseUrlInput.placeholder_text = "https://api.example.com/v1"
modelInput.placeholder_text = "gpt-4o-mini"
if modelInput.text.strip_edges().is_empty() or modelInput.text.strip_edges().begins_with("claude-"):
modelInput.text = "gpt-4o-mini"
if clearModelList:
modelOption.clear()
modelOption.visible = false
func _render_models(models: Array) -> void:
modelOption.clear()
var currentModel := modelInput.text.strip_edges()
var selectedIndex := -1
for modelVariant in models:
var modelId := _model_id_from_variant(modelVariant)
if modelId.is_empty():
continue
var index := modelOption.get_item_count()
modelOption.add_item(_model_label_from_variant(modelVariant, modelId))
modelOption.set_item_metadata(index, modelId)
if modelId == currentModel:
selectedIndex = index
if modelOption.get_item_count() <= 0:
modelOption.visible = false
_set_status("接口没有返回可用模型,可继续手动填写模型", true)
return
modelOption.visible = true
modelOption.select(selectedIndex if selectedIndex >= 0 else 0)
if currentModel.is_empty() or selectedIndex < 0:
modelInput.text = str(modelOption.get_item_metadata(modelOption.selected))
_set_status("已获取 %d 个可用模型" % modelOption.get_item_count(), false)
func _on_model_option_selected(index: int) -> void:
if index < 0:
return
modelInput.text = str(modelOption.get_item_metadata(index)).strip_edges()
func _selected_model_id() -> String:
var model := modelInput.text.strip_edges()
if not model.is_empty():
return model
if modelOption.visible and modelOption.selected >= 0:
return str(modelOption.get_item_metadata(modelOption.selected)).strip_edges()
return ""
func _model_id_from_variant(modelVariant: Variant) -> String:
if modelVariant is String:
return str(modelVariant).strip_edges()
if modelVariant is Dictionary:
var model: Dictionary = modelVariant
return str(model.get("id", "")).strip_edges()
return ""
func _model_label_from_variant(modelVariant: Variant, modelId: String) -> String:
if not (modelVariant is Dictionary):
return modelId
var model: Dictionary = modelVariant
var label := str(model.get("label", modelId)).strip_edges()
var ownedBy := str(model.get("owned_by", "")).strip_edges()
if not ownedBy.is_empty():
return "%s · %s" % [label, ownedBy]
return label
func _on_close_pressed() -> void:
recruitmentFrame.visible = false
_isSubmitting = false
_isFetchingModels = false
submitButton.disabled = false
fetchModelsButton.disabled = false
tokenInput.clear()
func _set_status(message: String, isError: bool) -> void:
statusLabel.text = message
statusLabel.add_theme_color_override("font_color", Color(0.68, 0.22, 0.18, 1) if isError else Color(0.32, 0.42, 0.48, 1))
func _get_cafe_companion_manager() -> Node:
return get_node_or_null("/root/CafeCompanionManager")
func _format_service_point_label(servicePointId: String) -> String:
if servicePointId.begins_with("ServiceIdlePoint"):
var pointNumber := _parse_point_number(servicePointId)
if pointNumber > 0:
return "%s号陪伴位" % _format_chinese_number(pointNumber)
return servicePointId
func _parse_point_number(value: String) -> int:
var digits := ""
for index in range(value.length()):
var character := value.substr(index, 1)
if character >= "0" and character <= "9":
digits += character
return int(digits) if not digits.is_empty() else 0
func _format_chinese_number(value: int) -> String:
const CHINESE_DIGITS: Array[String] = [
"", "", "", "", "", "", "", "", "", ""
]
if value <= 0:
return str(value)
if value < 10:
return CHINESE_DIGITS[value]
if value == 10:
return ""
if value < 20:
return "%s" % CHINESE_DIGITS[value - 10]
if value < 100:
var tens := value / 10
var ones := value % 10
return "%s%s" % [CHINESE_DIGITS[tens], CHINESE_DIGITS[ones]] if ones > 0 else "%s" % CHINESE_DIGITS[tens]
return str(value)