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)