> 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 => "Minimal Module";

    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] 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)
    { _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 服务

| 属性            | 类型                        | 可用性  | 说明                 |
| ------------- | ------------------------- | ---- | ------------------ |
| `Runtime`     | `ConvaiRuntime`           | 始终可用 | 此模块所属的运行时实例。       |
| `Events`      | `IEventHub`               | 始终可用 | 发布和订阅领域事件。         |
| `Agents`      | `IAgentRegistry`          | 始终可用 | 查询已注册的角色和玩家。       |
| `Transport`   | `ITransportProvider`      | 可能为空 | 平台相关的通信层。          |
| `Preferences` | `IRuntimePreferences`     | 可能为空 | 可变的运行时偏好设置。        |
| `Logger`      | `ILogger`                 | 可能为空 | 用于诊断的日志记录器。        |
| `RoomAudio`   | `IConvaiRoomAudioService` | 可能为空 | 麦克风与播放服务。          |
| `Credentials` | `ICredentialProvider`     | 可能为空 | API 密钥和服务器 URL 解析。 |

{% hint style="warning" %}
使用前始终 `Transport`, `Preferences`, `Logger`, `RoomAudio`以及 `Credentials` 检查空值。 `Events` 和 `Agents` 都保证非空。访问空服务会抛出一个 `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 => "Haptic Feedback";

    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] Started.", 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] Stopped.", 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 => "Audio Analysis";

    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 => "Haptic Feedback";
    // ... 实现其余 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` 返回 `为`，这表示所有已注册模块都已被发现，且运行时已经启动。

#### 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`               | 可选  |
| `Logger`            | `ILogger`                      | 可选  |

#### IConvaiPlayerDependencies

| 属性                       | 类型                              | 可用性 |
| ------------------------ | ------------------------------- | --- |
| `PlayerInputService`     | `IPlayerInputService`           | 可选  |
| `RuntimeSettingsService` | `IConvaiRuntimeSettingsService` | 可选  |
| `Logger`                 | `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] Turn completed.", 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 => "Biometric Correlation";
    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 => "Assessment Scoring";
    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 dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` 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>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
