> 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).

# 实现自定义模块

构建一个自定义运行时模块，该模块随 SDK 启动、访问其服务并响应角色和会话事件。

构建一个自定义模块，使其集成到 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.Domain.EventSystem;
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 IEventHub _events;
    private SubscriptionToken _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)
    {
        _events = ctx.Events;
        return 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] 角色 {e.CharacterId} 开始说话。");
        });
        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)
    { _events?.Unsubscribe(_sub); 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`      | 可能为空 | 特定平台的通信层。          |
| `首选项`   | `IRuntimePreferences`     | 可能为空 | 可变的运行时首选项。         |
| `日志记录器` | `ILogger`                 | 可能为空 | 用于诊断的日志记录器。        |
| `房间音频`  | `IConvaiRoomAudioService` | 可能为空 | 麦克风与播放服务。          |
| `凭据`    | `ICredentialProvider`     | 可能为空 | 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 IEventHub          _events;
    private SubscriptionToken  _subscription;

    public ValueTask RegisterAsync(IModuleContext context, CancellationToken ct = default)
    {
        _logger = context.Logger;
        _events = context.Events;
        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)
    {
        _events?.Unsubscribe(_subscription);
        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

| 属性      | 类型                             | 可用性 |
| ------- | ------------------------------ | --- |
| `事件总线`  | `IEventHub`                    | 必需  |
| `连接服务`  | `IConvaiRoomConnectionService` | 必需  |
| `音频服务`  | `IConvaiRoomAudioService`      | 必需  |
| `代理注册表` | `IAgentRegistry`               | 可选  |
| `日志记录器` | `ILogger`                      | 可选  |

#### IConvaiPlayerDependencies

| 属性        | 类型                              | 可用性 |
| --------- | ------------------------------- | --- |
| `玩家输入服务`  | `IPlayerInputService`           | 可选  |
| `运行时设置服务` | `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;
    private SubscriptionToken _turnCompletedToken;

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

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

    private void OnDestroy()
    {
        _events?.Unsubscribe(_turnCompletedToken);
    }
}
```

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

### 使用示例

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

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

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

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 IEventHub          _events;
    private SubscriptionToken  _emotionSubscription;
    private BiometricLogger    _bioLogger;

    public ValueTask RegisterAsync(IModuleContext context, CancellationToken ct = default)
    {
        _bioLogger = BiometricLogger.Instance;
        _events = context.Events;
        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)
    {
        _events?.Unsubscribe(_emotionSubscription);
        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;
using Convai.Domain.EventSystem;

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 IEventHub              _events;
    private SubscriptionToken      _actionSubscription;

    public ValueTask RegisterAsync(IModuleContext context, CancellationToken ct = default)
    {
        _scoreService = new AssessmentScoreService();
        context.ProvideModuleService<IAssessmentScoreService>(_scoreService);
        _events = context.Events;
        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)
    {
        _events?.Unsubscribe(_actionSubscription);
        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` 在……中为空 `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 by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
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>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

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.
