> For the complete documentation index, see [llms.txt](https://docs.convai.com/api-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.convai.com/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/advanced-topics/implement-a-custom-module.md).

# 实现自定义模块

构建一个自定义模块，将其与 Convai 运行时生命周期集成，访问 SDK 服务，并订阅领域事件。在开始之前，请阅读 [运行时模块系统](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/advanced-topics/extending-the-sdk.md) 以了解何时模块是合适的工具，以及生命周期状态如何映射到你的实现。

### 先决条件

* 一个可正常工作的 Convai 场景，包含一个 `ConvaiManager` 组件
* 具备 C# 熟练度，包括 async/await 和 Unity 的 `MonoBehaviour` 生命周期
* 熟悉 [运行时模块系统](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/advanced-topics/extending-the-sdk.md)

### 快速入门：最小模块

在阅读完整的接口契约之前，这里是构建一个可工作的模块的最短路径——一个 `MonoBehaviour` 它会自行注册并订阅一个 SDK 事件：

```csharp
// MinimalModule.cs
using System;
using System.Collections.Generic;
using System.Threading;
using Convai.Domain.DomainEvents.Runtime;
using Convai.Runtime.Components;
using Convai.Runtime.Core.Modules;
using UnityEngine;

public class MinimalModule : MonoBehaviour, IConvaiModule
{
    public string ModuleId    => "my-project.minimal";
    public string DisplayName => "最小模块";

    public IReadOnlyList<string> RequiredModules  => Array.Empty<string>();
    public IReadOnlyList<Type>   RequiredServices => Array.Empty<Type>();
    public IReadOnlyList<Type>   ProvidedServices => Array.Empty<Type>();
    public bool IsActive { get; private set; }

    private IDisposable _sub;

    private void Awake() => ConvaiManager.ActiveManager?.RegisterModule(this);
    private void OnDestroy() => ConvaiManager.ActiveManager?.UnregisterModule(this);

    public System.Threading.Tasks.ValueTask RegisterAsync(IModuleContext ctx, CancellationToken ct = default)
        => System.Threading.Tasks.ValueTask.CompletedTask;

    public System.Threading.Tasks.ValueTask StartAsync(IModuleContext ctx, CancellationToken ct = default)
    {
        _sub = ctx.Events.Subscribe<CharacterSpeechStateChanged>(e =>
        {
            if (e.IsSpeaking) Debug.Log($"[MinimalModule] Character {e.CharacterId} started speaking.");
        });
        IsActive = true;
        return System.Threading.Tasks.ValueTask.CompletedTask;
    }

    public System.Threading.Tasks.ValueTask PauseAsync(RuntimePauseReason r, CancellationToken ct = default)
    { IsActive = false; return System.Threading.Tasks.ValueTask.CompletedTask; }

    public System.Threading.Tasks.ValueTask ResumeAsync(CancellationToken ct = default)
    { IsActive = true; return System.Threading.Tasks.ValueTask.CompletedTask; }

    public System.Threading.Tasks.ValueTask StopAsync(CancellationToken ct = default)
    { _sub?.Dispose(); IsActive = false; return System.Threading.Tasks.ValueTask.CompletedTask; }
}
```

将此组件添加到场景中的任意 GameObject 上。完整的接口契约和高级模式如下。

### IConvaiModule 接口

```csharp
public interface IConvaiModule
{
    string ModuleId    { get; }
    string DisplayName { get; }
    IReadOnlyList<string> RequiredModules  { get; }
    IReadOnlyList<Type>   RequiredServices { get; }
    IReadOnlyList<Type>   ProvidedServices { get; }
    bool IsActive { get; }

    ValueTask RegisterAsync(IModuleContext context, CancellationToken ct = default);
    ValueTask StartAsync(IModuleContext context, CancellationToken ct = default);
    ValueTask PauseAsync(RuntimePauseReason reason, CancellationToken ct = default);
    ValueTask ResumeAsync(CancellationToken ct = default);
    ValueTask StopAsync(CancellationToken ct = default);
}
```

#### 生命周期方法参考

| 方法              | 何时调用                       | 做什么                                              |
| --------------- | -------------------------- | ------------------------------------------------ |
| `RegisterAsync` | 在运行时构建期间——在任何 `StartAsync` | 通过 `context.ProvideModuleService<T>()`注册服务。订阅事件。 |
| `StartAsync`    | 运行时启动——在所有模块注册之后           | 启动活动行为：开始处理、初始化硬件、启动协程。                          |
| `PauseAsync`    | 运行时暂停（应用失去焦点、主动暂停）         | 停止处理。使用 `RuntimePauseReason` 来区分原因。              |
| `ResumeAsync`   | 运行时恢复                      | 恢复在以下状态中暂停的处理 `PauseAsync`.                      |
| `StopAsync`     | 运行时停止或模块被移除                | 清理：取消订阅、停止协程、释放资源。                               |

### IModuleContext 服务

| 属性      | 类型                        | 可用性      | 描述                 |
| ------- | ------------------------- | -------- | ------------------ |
| `运行时`   | `ConvaiRuntime`           | 总是       | 此模块所属的运行时实例。       |
| `事件`    | `IEventHub`               | 总是       | 发布并订阅领域事件。         |
| `代理`    | `IAgentRegistry`          | 总是       | 查询已注册的角色和玩家。       |
| `传输`    | `ITransportProvider`      | 可能为 null | 平台特定的通信层。          |
| `首选项`   | `IRuntimePreferences`     | 可能为 null | 可变的运行时首选项。         |
| `日志记录器` | `ILogger`                 | 可能为 null | 用于诊断的日志记录器。        |
| `房间音频`  | `IConvaiRoomAudioService` | 可能为 null | 麦克风和播放服务。          |
| `凭据`    | `ICredentialProvider`     | 可能为 null | API 密钥和服务器 URL 解析。 |

{% hint style="warning" %}
始终进行空值检查 `传输`, `首选项`, `日志记录器`, `房间音频`，并且 `凭据` 在使用之前。 `事件` 和 `代理` 保证非空。访问空服务会抛出一个 `NullReferenceException` 从而中止模块生命周期。
{% endhint %}

有关可订阅领域事件的完整列表，请参阅 [事件系统](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/core-concepts/event-system.md).

### 实现一个模块

#### 事件订阅者示例

一个订阅领域事件的模块，当角色说话时触发触觉反馈。

```csharp
// HapticFeedbackModule.cs
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Convai.Domain.DomainEvents.Runtime;
using Convai.Domain.EventSystem;
using Convai.Domain.Logging;
using Convai.Runtime.Core.Modules;

public class HapticFeedbackModule : IConvaiModule
{
    public string ModuleId    => "my-company.haptic-feedback";
    public string DisplayName => "触觉反馈";

    public IReadOnlyList<string> RequiredModules  => Array.Empty<string>();
    public IReadOnlyList<Type>   RequiredServices => Array.Empty<Type>();
    public IReadOnlyList<Type>   ProvidedServices => Array.Empty<Type>();

    public bool IsActive { get; private set; }

    private ILogger      _logger;
    private IDisposable  _subscription;

    public ValueTask RegisterAsync(IModuleContext context, CancellationToken ct = default)
    {
        _logger = context.Logger;
        return ValueTask.CompletedTask;
    }

    public ValueTask StartAsync(IModuleContext context, CancellationToken ct = default)
    {
        _subscription = context.Events.Subscribe<CharacterSpeechStateChanged>(OnSpeechStateChanged);
        IsActive = true;
        _logger?.Debug("[HapticFeedbackModule] 已启动。", LogCategory.SDK);
        return ValueTask.CompletedTask;
    }

    public ValueTask PauseAsync(RuntimePauseReason reason, CancellationToken ct = default)
    {
        IsActive = false;
        return ValueTask.CompletedTask;
    }

    public ValueTask ResumeAsync(CancellationToken ct = default)
    {
        IsActive = true;
        return ValueTask.CompletedTask;
    }

    public ValueTask StopAsync(CancellationToken ct = default)
    {
        _subscription?.Dispose();
        IsActive = false;
        _logger?.Debug("[HapticFeedbackModule] 已停止。", LogCategory.SDK);
        return ValueTask.CompletedTask;
    }

    private void OnSpeechStateChanged(CharacterSpeechStateChanged e)
    {
        if (!IsActive || !e.IsSpeaking) return;
        HapticService.Pulse(HapticPattern.Soft);
    }
}
```

#### 服务提供者和消费者示例

模块在 `ProvidedServices`中声明其提供的服务，并在 `RegisterAsync`中注册该实例，而消费模块则通过 `TryGetModuleService<T>`.

```csharp
// AudioAnalysisModule.cs — 提供 IAudioAnalysisService
public class AudioAnalysisModule : IConvaiModule
{
    public string ModuleId    => "my-company.audio-analysis";
    public string DisplayName => "音频分析";

    public IReadOnlyList<string> RequiredModules  => Array.Empty<string>();
    public IReadOnlyList<Type>   RequiredServices => Array.Empty<Type>();
    public IReadOnlyList<Type>   ProvidedServices => new[] { typeof(IAudioAnalysisService) };

    public bool IsActive { get; private set; }
    private AudioAnalysisService _service;

    public ValueTask RegisterAsync(IModuleContext context, CancellationToken ct = default)
    {
        _service = new AudioAnalysisService(context.RoomAudio);
        context.ProvideModuleService<IAudioAnalysisService>(_service); // 必须在 RegisterAsync 中，而不是 StartAsync 中。
        return ValueTask.CompletedTask;
    }

    public ValueTask StartAsync(IModuleContext context, CancellationToken ct = default)
    { IsActive = true; _service.Start(); return ValueTask.CompletedTask; }

    public ValueTask PauseAsync(RuntimePauseReason reason, CancellationToken ct = default)
    { IsActive = false; return ValueTask.CompletedTask; }

    public ValueTask ResumeAsync(CancellationToken ct = default)
    { IsActive = true; return ValueTask.CompletedTask; }

    public ValueTask StopAsync(CancellationToken ct = default)
    { IsActive = false; _service.Stop(); return ValueTask.CompletedTask; }
}
```

一个消费模块：

```csharp
// VisualizerModule.cs — 消耗 IAudioAnalysisService
public class VisualizerModule : IConvaiModule
{
    public IReadOnlyList<Type> RequiredServices => new[] { typeof(IAudioAnalysisService) };
    // ... 其他接口成员 ...

    public ValueTask StartAsync(IModuleContext context, CancellationToken ct = default)
    {
        if (context.TryGetModuleService<IAudioAnalysisService>(out var analysis))
        {
            // 此处 analysis 保证非空。
        }
        return ValueTask.CompletedTask;
    }
}
```

{% hint style="warning" %}
始终使用 `TryGetModuleService` ——不要假定服务一定存在。如果 `AudioAnalysisModule` 未注册， `TryGetModuleService` 返回 `false` 而不会抛出异常，从而让 `VisualizerModule` 优雅降级。
{% endhint %}

### 注册模块

#### MonoBehaviour 自注册（推荐）

将模块作为组件附加到任意 GameObject 上。它会通过 `ConvaiManager` 于 `Awake`.

```csharp
// HapticFeedbackBridge.cs
using Convai.Runtime.Components;
using Convai.Runtime.Core.Modules;
using UnityEngine;

public class HapticFeedbackBridge : MonoBehaviour, IConvaiModule
{
    public string ModuleId    => "my-company.haptic-feedback";
    public string DisplayName => "触觉反馈";
    // ... 实现剩余的 IConvaiModule 成员 ...

    private void Awake()
    {
        // ConvaiManager.Awake() 以执行顺序 -1100 运行。
        // 这个 Awake() 以默认顺序 0 运行——ConvaiManager.ActiveManager 已经被设置。
        ConvaiManager.ActiveManager?.RegisterModule(this);
    }

    private void OnDestroy()
    {
        ConvaiManager.ActiveManager?.UnregisterModule(this);
    }
}
```

之后 `ConvaiManager.Start()` 完成后， `ConvaiManager.ActiveManager.IsInitialized` 返回 `true`，这表示所有已注册模块都已被发现，且运行时已经启动。

#### CreateRuntimeBuilder 重写

当你希望将所有自定义集中在一处时，或者当模块不是一个 `MonoBehaviour`.

```csharp
// CustomRuntimeManager.cs
using Convai.Runtime.Components;
using Convai.Runtime.Core;

public class CustomRuntimeManager : ConvaiManager
{
    protected override ConvaiRuntimeBuilder CreateRuntimeBuilder()
    {
        ConvaiRuntimeBuilder builder = base.CreateRuntimeBuilder();
        builder.AddModule(new HapticFeedbackModule());
        builder.AddModule(new AudioAnalysisModule());
        return builder;
    }
}
```

### 使用依赖注入模式

位于 `ConvaiCharacter` 或 `ConvaiPlayer` GameObject 上的组件可以通过实现 `IInjectable<TDependencies>`自动接收 SDK 服务。SDK 会在角色或玩家注册到运行时之后注入依赖。

#### IInjectable\<TDependencies>

```csharp
public interface IInjectable<in TDependencies> where TDependencies : class
{
    int  InjectionOrder => 0;                        // 越小越先注入。默认值 0。
    void InjectDependencies(TDependencies dependencies);
}
```

#### IConvaiCharacterDependencies

| 属性                  | 类型                             | 可用性 |
| ------------------- | ------------------------------ | --- |
| `EventHub`          | `IEventHub`                    | 必需  |
| `ConnectionService` | `IConvaiRoomConnectionService` | 必需  |
| `AudioService`      | `IConvaiRoomAudioService`      | 必需  |
| `AgentRegistry`     | `IAgentRegistry`               | 可选  |
| `日志记录器`             | `ILogger`                      | 可选  |

#### IConvaiPlayerDependencies

| 属性                       | 类型                              | 可用性 |
| ------------------------ | ------------------------------- | --- |
| `PlayerInputService`     | `IPlayerInputService`           | 可选  |
| `RuntimeSettingsService` | `IConvaiRuntimeSettingsService` | 可选  |
| `日志记录器`                  | `ILogger`                       | 可选  |

#### 编写一个可注入组件

```csharp
// CharacterHealthIndicator.cs
using Convai.Domain.DomainEvents.Runtime;
using Convai.Domain.EventSystem;
using Convai.Domain.Logging;
using Convai.Runtime.Core.DependencyInjection;
using UnityEngine;

public class CharacterHealthIndicator : MonoBehaviour,
    IInjectable<IConvaiCharacterDependencies>
{
    public int InjectionOrder => 0;

    private IEventHub _events;
    private ILogger   _logger;

    public void InjectDependencies(IConvaiCharacterDependencies dependencies)
    {
        _events = dependencies.EventHub;
        _logger = dependencies.Logger;
        _events.Subscribe<CharacterTurnCompleted>(OnTurnCompleted);
    }

    private void OnTurnCompleted(CharacterTurnCompleted e)
    {
        _logger?.Debug("[CharacterHealthIndicator] 回合已完成。", LogCategory.Character);
        // 在此更新生命值指示器 UI。
    }

    private void OnDestroy()
    {
        _events?.Unsubscribe<CharacterTurnCompleted>(OnTurnCompleted);
    }
}
```

将此组件添加到与 `ConvaiCharacter`相同的 GameObject 上。SDK 会在角色注册期间自动调用 `InjectDependencies` 。

### 使用示例

#### 示例 1：用于医疗模拟的生物特征关联模块

将角色情绪数据与生物特征传感器读数一起记录，以便进行会后分析。

```csharp
using Convai.Domain.DomainEvents.Runtime;

public class BiometricCorrelationModule : IConvaiModule
{
    public string ModuleId    => "medsim.biometric-correlation";
    public string DisplayName => "生物特征关联";
    public IReadOnlyList<string> RequiredModules  => Array.Empty<string>();
    public IReadOnlyList<Type>   RequiredServices => Array.Empty<Type>();
    public IReadOnlyList<Type>   ProvidedServices => Array.Empty<Type>();
    public bool IsActive { get; private set; }

    private IDisposable    _emotionSubscription;
    private BiometricLogger _bioLogger;

    public ValueTask RegisterAsync(IModuleContext context, CancellationToken ct = default)
    {
        _bioLogger = BiometricLogger.Instance;
        return ValueTask.CompletedTask;
    }

    public ValueTask StartAsync(IModuleContext context, CancellationToken ct = default)
    {
        _emotionSubscription = context.Events.Subscribe<CharacterEmotionChanged>(OnEmotionChanged);
        IsActive = true;
        return ValueTask.CompletedTask;
    }

    public ValueTask PauseAsync(RuntimePauseReason reason, CancellationToken ct = default)
    { IsActive = false; return ValueTask.CompletedTask; }

    public ValueTask ResumeAsync(CancellationToken ct = default)
    { IsActive = true; return ValueTask.CompletedTask; }

    public ValueTask StopAsync(CancellationToken ct = default)
    {
        _emotionSubscription?.Dispose();
        IsActive = false;
        return ValueTask.CompletedTask;
    }

    private void OnEmotionChanged(CharacterEmotionChanged e)
    {
        if (!IsActive) return;
        _bioLogger.Record(timestamp: e.Timestamp, emotionLabel: e.Emotion, intensity: e.Intensity);
    }
}
```

#### 示例 2：用于工业培训的评估评分模块

根据评分量表跟踪角色触发的动作，并通过以下方式将评分服务暴露给其他模块 `ProvideModuleService`.

```csharp
using Convai.Domain.DomainEvents.Runtime;

public class ScoringModule : IConvaiModule
{
    public string ModuleId    => "industrial.scoring";
    public string DisplayName => "评估评分";
    public IReadOnlyList<string> RequiredModules  => Array.Empty<string>();
    public IReadOnlyList<Type>   RequiredServices => Array.Empty<Type>();
    public IReadOnlyList<Type>   ProvidedServices => new[] { typeof(IAssessmentScoreService) };
    public bool IsActive { get; private set; }

    private AssessmentScoreService _scoreService;
    private IDisposable            _actionSubscription;

    public ValueTask RegisterAsync(IModuleContext context, CancellationToken ct = default)
    {
        _scoreService = new AssessmentScoreService();
        context.ProvideModuleService<IAssessmentScoreService>(_scoreService);
        return ValueTask.CompletedTask;
    }

    public ValueTask StartAsync(IModuleContext context, CancellationToken ct = default)
    {
        _actionSubscription = context.Events.Subscribe<CharacterActionReceived>(OnActionReceived);
        IsActive = true;
        return ValueTask.CompletedTask;
    }

    public ValueTask PauseAsync(RuntimePauseReason reason, CancellationToken ct = default)
    { IsActive = false; return ValueTask.CompletedTask; }

    public ValueTask ResumeAsync(CancellationToken ct = default)
    { IsActive = true; return ValueTask.CompletedTask; }

    public ValueTask StopAsync(CancellationToken ct = default)
    {
        _actionSubscription?.Dispose();
        IsActive = false;
        return ValueTask.CompletedTask;
    }

    private void OnActionReceived(CharacterActionReceived e)
    {
        if (!IsActive) return;
        foreach (var action in e.Actions)
            _scoreService.RecordAction(action.Name, action.Target, e.Timestamp);
    }
}
```

### 故障排查

| 症状                                             | 可能原因                                                          | 修复方法                                                                               |
| ---------------------------------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| 模块的 `StartAsync` 从未被调用                         | `RegisterAsync` 抛出了未处理的异常；运行时会静默中止模块启动                        | 将 `RegisterAsync` 主体包裹在 try-catch 中，并明确记录日志。                                       |
| `TryGetModuleService<T>` 返回 `false` 意外地        | `ProvideModuleService<T>` 是在 `StartAsync` 而不是 `RegisterAsync` | 将 `ProvideModuleService<T>` 为 `RegisterAsync` ——在任何模块的 `StartAsync` 运行后更新。         |
| 模块已启动但错过了早期事件                                  | 在中订阅 `RegisterAsync` 但事件在启动期间于 `StartAsync`                   | 将订阅移到 `StartAsync`，或者使用 `IsActive` 在处理程序中进行检查。                                     |
| `RequiredModules` 条目会导致启动错误                    | 列出的模块 ID 在运行时构建之前未注册                                          | 请检查模块 ID 字符串是否完全匹配——ID 区分大小写。                                                      |
| `InjectDependencies` 从未在 `IInjectable` 组件      | 组件不在层级结构中的一个 GameObject 上 `ConvaiCharacter` 层级结构              | `IInjectable<IConvaiCharacterDependencies>` 仅适用于作为角色子级的 GameObject。                |
| `ConvaiManager.ActiveManager` 在中为 null `Awake` | 管理器的 `Awake` 尚未在执行顺序 −1100 运行                                 | 在中注册模块 `Start()` ，或者使用 `ConvaiManager.ActiveManager?.RegisterModule(this)` 具有空安全性。 |

### 下一步

{% content-ref url="/pages/fef9ce3a50abea0da1b1bc0e993c3308a067a94a" %}
[日志、指标与重试策略](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/advanced-topics/performance-and-optimization.md)
{% endcontent-ref %}

{% content-ref url="/pages/0bd691fc4d8a06b0dbafd0f28b11f39be6f57f9a" %}
[事件系统](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/core-concepts/event-system.md)
{% endcontent-ref %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.convai.com/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/advanced-topics/implement-a-custom-module.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
