# KBEngineNex-Plugin-Bag
**Repository Path**: KBEngineLab/kbenginenex-plugin-bag
## Basic Information
- **Project Name**: KBEngineNex-Plugin-Bag
- **Description**: No description available
- **Primary Language**: Python
- **License**: Apache-2.0
- **Default Branch**: main
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-06-07
- **Last Updated**: 2026-06-26
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# Bag 插件使用帮助
Bag 是一个挂载在 `Avatar` 上的背包组件插件。它把背包从“独立实体”改成“组件 + 独立数据表”的模式,适合做装备、道具、材料、邮件附件、离线发奖、商城补单和 GM 工具。
## 1. 这套插件解决什么问题
这套实现的目标很直接:
- 背包挂在 `Avatar` 组件上,跟玩家账号生命周期一致,业务侧直接用 `self.bag`。
- 背包实例数据落在独立数据库表里,不依赖 KBE 自动持久化属性。
- 同一玩家的写操作按顺序串行,避免 SQL 异步回调乱序。
- 客户端只负责读和展示,真正的写逻辑都在服务端。
- 插件自带 `BagManager`,负责 watchdog 扫描,避免某条回调丢失后把队列卡住。
- 背包容量、最大堆叠、绑定、过期、锁定这些常见业务点都留了结构。
如果你只是想快速接入,可以先看第 3 节和第 5 节。
## 2. 文件结构
```text
plugins/Bag/
plugin.json
entity_defs/
BagManager.def
types.xml
components/
BagComponent.def
base/
BagManager.py
plugin_entry.py
components/
BagComponent.py
bots/
plugin_entry.py
components/
BagComponent.py
common/
bag_model.py
bag_service.py
bag_storage.py
```
### 关键文件
- `plugin.json`:插件注册入口。
- `entity_defs/types.xml`:背包类型定义,决定 `BagItem` 和 `BagItems` 的结构。
- `entity_defs/components/BagComponent.def`:`Avatar` 上的背包组件接口。
- `base/components/BagComponent.py`:base 侧组件实现。
- `common/bag_service.py`:背包核心逻辑。
- `common/bag_storage.py`:SQL 生成和结果解码。
- `base/plugin_entry.py`:base 插件生命周期入口。
## 3. 如何接入 Avatar
在 `Avatar.def` 里挂载组件:
```xml
BagComponent
false
```
含义很简单:
- `Type` 必须是 `BagComponent`。
- `Persistent=false` 表示不走 KBE 自动持久化整套组件状态,背包数据由数据库管理。
挂上以后,`Avatar` 的 base 侧就会多一个 `bag` 组件调用入口,客户端侧也会多一个 `bag` 组件回调入口。
## 4. 启动流程
插件启动时的流程大概是:
1. `plugin.json` 注册插件。
2. `base/plugin_entry.py` 初始化插件。
3. `onComponentReady()` 时创建三张表:
- `kbe_plugin_bag_items`
- `kbe_plugin_bag_op_logs`
- `kbe_plugin_bag_meta`
4. 同时创建一个 `BagManager` 常驻实体。
5. `BagManager` 用定时器周期性执行 `tickBagQueues()`。
你不用手动启动 watchdog。只要插件加载正常,它就会工作。
## 5. 最快上手
### 5.1 给玩家加物品
```python
bag = self.bag
bag.addItem(1001, 3, 1)
bag.addItem(2001, 1, 0, '{"atk": 12, "quality": "rare"}')
```
### 5.2 查询背包
```python
bag.listItems(callback)
bag.pageItems(1, 20, callback)
bag.getItem(bid, callback)
bag.getItemCount(1001, callback)
```
### 5.3 设置容量
```python
bag.setCapacity(120)
```
### 5.4 多物品交易
```python
bag.transferItems(targetDBID, [{"bid": bid1, "count": 1}, {"bid": bid2, "count": 3}])
```
## 6. 数据模型
### 6.1 `BagItem`
`BagItem` 是单个物品实例,字段如下:
- `bid`:实例主键,数据库自增 ID。
- `itemID`:物品配置 ID。
- `count`:数量。
- `bagIndex`:背包格子序号。
- `stackable`:是否允许堆叠,`1` 允许,`0` 不允许。
- `maxStack`:单格最大堆叠数量。
- `bindType`:绑定类型。
- `expireAt`:过期时间戳,`0` 表示不过期。
- `locked`:锁定状态,`1` 表示锁定。
- `extra`:附加属性 JSON 字符串。
### 6.2 `BagItems`
`BagItems` 是 `ARRAY`,用于:
- `onBagList(items)`
- `onBagPage(page, pageSize, total, items)`
### 6.3 物品排序规则
客户端展示和服务端列表都会按以下顺序排序:
1. `bagIndex`
2. `bid`
## 7. Base 组件接口
这些接口定义在 `entity_defs/components/BagComponent.def` 的 `BaseMethods` 中。
### 7.1 写接口
- `addItem(itemID, count, stackable=1, extra="")`
- 服务端调用。
- 不暴露给客户端。
- 会优先叠加到同 `itemID`、同 `extra` 的未满堆叠格。
- `removeItem(bid, count)`
- 服务端调用。
- 不暴露给客户端。
- 按实例扣数量,扣到 0 会删除该行。
- `setCapacity(capacity)`
- 服务端调用。
- `0` 表示不限制容量。
- 会把容量同步到 `kbe_plugin_bag_meta`。
- `setCallbackSwitch(callbackName, enabled)`
- 服务端调用。
- `callbackName` 支持:
- `list`
- `updated`
- `page`
- `error`
- `enabled` 传 `0/1`。
- `splitItem(bid, count)`
- 客户端可调用。
- 服务端会校验数量和容量。
- `swapItem(bid1, bid2)`
- 客户端可调用。
- 交换两个实例位置。
- `moveItem(bid, bagIndex)`
- 客户端可调用。
- 目标格空则移动,有物品则交换。
- `mergeItem(fromBID, toBID)`
- 客户端可调用。
- 要求 `itemID`、`extra`、`stackable` 一致,并且合并后不超过 `maxStack`。
- `sortItems()`
- 客户端可调用。
- 按 `itemID` 重排 `bagIndex`。
- `transferItems(targetDBID, itemsJson)`
- 服务端调用。
- 支持一次交易多个物品。
- `itemsJson` 形如:
```json
[{"bid": 1, "count": 2}, {"bid": 2, "count": 1}]
```
- `clear()`
- 服务端调用。
- 不暴露给客户端。
### 7.2 读接口
- `requestBagList()`
- 客户端请求完整背包。
- `requestBagPage(page, pageSize)`
- 客户端请求分页背包。
### 7.3 安全建议
- 奖励发放、扣除、商城补单、邮件附件、GM 指令,都应该走服务端写接口。
- 客户端不要直接拿到 `addItem`、`removeItem`、`clear` 这类高危写入口。
- `splitItem`、`mergeItem`、`moveItem`、`sortItems` 可以开放给客户端,但必须由服务端校验物品归属和规则。
## 8. 服务端 Python API
如果你不想走 `Avatar.bag` 组件,也可以直接按 `databaseID` 取服务对象;日常业务更推荐直接用 `self.bag`。这个入口适合离线发奖、后台补单、GM 工具和不持有 Avatar 实例的业务:
```python
from plugins.Bag.common.bag_service import getBagForEntityID
bag = getBagForEntityID(databaseID)
bag.addItem(1001, 3, 1, "", opID="mail_1001", reason="MAIL")
bag.removeItem(bid, 1, opID="gm_1002", reason="GM")
```
如果你在 `Avatar` 上下文里,还是直接这样写最顺:
```python
bag = self.bag
```
### 8.1 常用方法
- `bag.addItem(itemID, count, stackable=1, extra="", callback=None, opID="", reason="ADD", context="", maxStack=99)`
- `bag.removeItem(bid, count, callback=None, opID="", reason="REMOVE", context="")`
- `bag.splitItem(bid, count, callback=None, opID="", reason="SPLIT", context="")`
- `bag.swapItem(bid1, bid2, callback=None, opID="", reason="SWAP", context="")`
- `bag.moveItem(bid, bagIndex, callback=None, opID="", reason="MOVE", context="")`
- `bag.mergeItem(fromBID, toBID, callback=None, opID="", reason="MERGE", context="")`
- `bag.sortItems(callback=None, opID="", reason="SORT", context="")`
- `bag.clear(callback=None, opID="", reason="CLEAR", context="")`
- `bag.setCapacity(capacity, callback=None)`
- `bag.transferItems(targetDBID, items, callback=None, opID="", reason="TRANSFER", context="")`
- `bag.listItems(callback)`
- `bag.pageItems(page, pageSize, callback)`
- `bag.getItem(bid, callback)`
- `bag.getItemCount(itemID, callback)`
### 8.2 回调约定
写接口回调统一形如:
```python
callback(success, op, index, item, message)
```
读接口回调按各自方法签名返回。
### 8.3 `items` 参数格式
`transferItems()` 既支持 Python list,也支持 JSON 字符串。
Python 侧推荐:
```python
bag.transferItems(targetDBID, [{"bid": 10001, "count": 1}, {"bid": 10002, "count": 3}])
```
如果你从 KBE RPC 直接传参,建议传 JSON 字符串:
```python
avatar.bag.transferItems(targetDBID, '[{"bid":10001,"count":1},{"bid":10002,"count":3}]')
```
## 9. 客户端回调
客户端和 bots 侧组件都能收到这些回调:
- `onBagList(items)`
- `onBagUpdated(op, index, item)`
- `onBagPage(page, pageSize, total, items)`
- `onBagError(message)`
### 9.1 回调含义
- `onBagList(items)`:全量背包。
- `onBagUpdated(op, index, item)`:单条增量。
- `onBagPage(page, pageSize, total, items)`:分页结果。
- `onBagError(message)`:错误提示。
### 9.2 `onBagUpdated` 的 `op`
- `1`:新增
- `2`:更新
- `3`:删除
- `4`:清空
- `5`:移动/交换位置
- `6`:拆分
- `7`:合并
- `8`:整理
- `9`:多物品交易
### 9.3 回调开关
这四个回调可以分别关掉:
- `notifyBagList`
- `notifyBagUpdated`
- `notifyBagPage`
- `notifyBagError`
示例:
```python
avatar.bag.notifyBagUpdated = 0
avatar.bag.setCallbackSwitch("page", 0)
```
## 10. 容量和堆叠
### 10.1 容量
`capacity` 是背包容量,单位是格子数。
- `0`:不限制容量。
- `>0`:最多允许这么多条实例物品存在于背包里。
`setCapacity(capacity)` 会把容量写入 `kbe_plugin_bag_meta`,后续这些操作都会检查容量:
- `addItem()`
- `splitItem()`
- `transferItems()`
### 10.2 堆叠
`maxStack` 是单格最大堆叠数。
规则如下:
- `stackable=1` 时,`addItem()` 会优先叠加到同 `itemID`、同 `extra` 的未满堆叠格。
- 如果单个堆叠格已经满了,剩余数量会自动拆到新格子。
- `stackable=0` 时,每次都会新增独立实例。
### 10.3 绑定、过期、锁定
`bindType`、`expireAt`、`locked` 已经进入 `BagItem` 和数据库结构。
当前插件只负责:
- 保存
- 读取
- 透传
- 同步
真正的业务规则,例如:
- 是否允许交易
- 是否允许删除
- 到期后怎么处理
- 锁定状态下是否允许整理
这些建议由业务层继续接。
## 11. 数据库表
插件会创建三张表:
```sql
CREATE TABLE IF NOT EXISTS kbe_plugin_bag_items (...)
CREATE TABLE IF NOT EXISTS kbe_plugin_bag_op_logs (...)
CREATE TABLE IF NOT EXISTS kbe_plugin_bag_meta (...)
```
### 11.1 `kbe_plugin_bag_items`
用途:保存所有背包实例物品。
核心字段:
- `bid`
- `ownerDBID`
- `itemID`
- `count`
- `bagIndex`
- `stackable`
- `maxStack`
- `bindType`
- `expireAt`
- `locked`
- `extra`
说明:
- `bid` 是主键。
- `ownerDBID + itemID` 不是唯一键。
- 同一种物品可以拆成多个实例。
### 11.2 `kbe_plugin_bag_op_logs`
用途:保存背包操作日志。
核心字段:
- `opID`
- `ownerDBID`
- `targetDBID`
- `opType`
- `bid`
- `targetBID`
- `itemID`
- `count`
- `beforeCount`
- `afterCount`
- `beforeIndex`
- `afterIndex`
- `status`
- `reason`
- `context`
### 11.3 `kbe_plugin_bag_meta`
用途:保存背包元数据,目前主要是容量。
字段:
- `ownerDBID`
- `capacity`
## 12. 写操作队列
所有公开写接口都会先进入 `ownerDBID` 维度的串行队列:
- `addItem`
- `removeItem`
- `splitItem`
- `swapItem`
- `moveItem`
- `mergeItem`
- `sortItems`
- `clear`
- `transferItems`
### 12.1 为什么要队列
KBE 的 `executeRawDatabaseCommand` 是异步的。如果同一玩家连续点很多次:
- 拖拽
- 拆分
- 合并
- 整理
没有队列时,后发请求有可能先完成,造成:
- 数据库位置乱序
- 客户端增量顺序乱掉
- 业务日志和实际状态对不上
### 12.2 队列粒度
当前粒度是:
```text
一次公开写 API 调用 = 一个 Operation
```
这意味着:
- 不会自动把多次拖拽合并成一批。
- 不会自动把多次整理合并成一批。
- 每个操作的日志和回调都保留独立边界。
### 12.3 超时保护
默认超时时间是 `30s`。
如果某条 raw DB 回调丢了:
- 会打 `ERROR`
- 当前操作会失败
- 队列会继续跑下一条
插件里的 `BagManager` 会周期性调用 `tickBagQueues()`。
## 13. 操作日志
成功修改背包后会写操作日志。普通单人操作都是“先改背包,再写日志”。
### 13.1 记录什么
- 来源:`opID`
- 谁操作:`ownerDBID`
- 目标玩家:`targetDBID`
- 操作类型:`opType`
- 物品变化:`bid`、`targetBID`、`itemID`、`count`
- 数量变化:`beforeCount`、`afterCount`
- 位置变化:`beforeIndex`、`afterIndex`
- 额外信息:`reason`、`context`
### 13.2 重要说明
日志写失败不会把已经成功的背包操作回滚成失败,但会打 `ERROR`。
### 13.3 日志分级
背包操作日志支持三级过滤,可以通过 `setBagLogConfig(logLevel=...)` 在运行时动态调整。
| 等级 | 常量 | 记录范围 |
| ---- | ---- | -------- |
| L1 | `BAG_LOG_LEVEL_L1` | 添加、删除、清空、交易 |
| L2 | `BAG_LOG_LEVEL_L2` | L1 + 拆分、合并 |
| L3 | `BAG_LOG_LEVEL_L3` | 全部操作(L2 + 移动、交换、整理) |
各操作类型与等级的对应关系:
```python
# 内置映射(在 bag_service.py 中)
_BAG_LOG_TYPE_LEVEL = {
"ADD": L1, # 添加物品
"REMOVE": L1, # 删除物品
"CLEAR": L1, # 清空背包
"TRANSFER": L1, # 多物品交易
"SPLIT": L2, # 拆分
"MERGE": L2, # 合并
"MOVE": L3, # 移动/交换位置
"SWAP": L3, # 交换两个物品位置
"SORT": L3, # 整理
}
```
默认等级为 L3,即记录所有操作。如果你只需要追踪核心变动,建议切到 L2 或 L1:
- **L1**:只记录资产增减和交易,适合后台审计。
- **L2**:额外记录拆分/合并,适合排查堆叠问题。
- **L3**:记录全部操作(含移动、整理),适合开发调试。
### 13.4 输出类型
日志支持两种输出目标,通过 `setBagLogConfig(outputType=...)` 切换。
| 类型 | 常量 | 说明 |
| ---- | ---- | ---- |
| 数据库 | `BAG_LOG_OUTPUT_DATABASE` (1) | 写入 `kbe_plugin_bag_op_logs` 表,支持 SQL 查询与审计追溯 |
| 文件 | `BAG_LOG_OUTPUT_FILE` (2) | 写入磁盘日志文件,支持日期+大小滚动分割 |
默认输出到数据库。两种输出可以随时切换,不需要重启服务。
### 13.5 文件日志参数
当 `outputType` 设为文件时,日志会写入本地磁盘,并自动按**日期**和**文件大小**双重规则滚动分割。
| 参数 | 默认值 | 说明 |
| ---- | ------ | ---- |
| `filePath` | `logs/plugins/bag/bag.log` | 日志文件路径。目录不存在时会自动创建 |
| `maxBytes` | `10485760` (10 MB) | 单个日志文件最大字节数。超过则滚动到新文件 |
| `backupCount` | `30` | 历史日志文件最大保留数量。超出后自动删除最旧文件 |
| `encoding` | `utf-8` | 日志文件编码 |
滚动规则:
- **日期滚动**:跨天自动生成 `bag.2026-05-29.log` 格式的新文件。
- **大小滚动**:同一天内文件超过 `maxBytes` 时,追加编号为 `bag.2026-05-29.1.log`。
- **清理策略**:历史文件总数超过 `backupCount` 时,按修改时间删除最旧的文件。
日志输出格式(行业标准 key=value 管道格式):
```text
2026-05-29 14:30:15 | INFO | bag-op|level=L1|opType=ADD|ownerDBID=10001|targetDBID=0|bid=42|...
```
### 13.6 配置 API
所有配置通过 Python 方法完成,不需要手动编辑 JSON。可以在 `plugin_entry.py`、业务脚本或任何持有 Avatar 的地方调用。
**完整配置入口:**
```python
from plugins.Bag.common.bag_service import setBagLogConfig
setBagLogConfig(
logLevel=2, # 只记录 L2 及以上(含 ADD/REMOVE/CLEAR/TRANSFER/SPLIT/MERGE)
outputType=2, # 输出到文件
filePath="logs/plugins/bag/bag.log",
maxBytes=10 * 1024 * 1024, # 10 MB
backupCount=30,
encoding="utf-8",
)
```
**快捷方法:**
```python
from plugins.Bag.common.bag_service import (
setBagLogLevel,
setBagLogOutputType,
setBagLogFileConfig,
getBagLogConfig,
)
# 只改分级
setBagLogLevel(2)
# 只改输出类型
setBagLogOutputType(1) # 切回数据库
# 只改文件参数
setBagLogFileConfig(
filePath="logs/plugins/bag/bag.log",
maxBytes=5 * 1024 * 1024, # 5 MB
backupCount=10,
)
# 读取当前配置
config = getBagLogConfig()
print(config)
# {"level": 2, "outputType": 1, "filePath": "logs/plugins/bag/bag.log", "maxBytes": 5242880, ...}
```
所有参数均可选传,只传需要改的项,未传项保持原值不变。
### 13.7 配置示例
**示例 1:生产环境只记数据库、只记核心变动**
```python
from plugins.Bag.common.bag_service import setBagLogConfig
setBagLogConfig(logLevel=1, outputType=1)
```
- 操作类型:ADD、REMOVE、CLEAR、TRANSFER
- 输出:数据库 `kbe_plugin_bag_op_logs` 表
**示例 2:日志落盘,完整追踪所有操作**
```python
setBagLogConfig(logLevel=3, outputType=2, filePath="logs/plugins/bag/bag.log")
```
- 操作类型:全部
- 输出:文件,自动滚动
**示例 3:开发调试时切换到文件、只记核心操作**
```python
setBagLogConfig(logLevel=2, outputType=2, filePath="logs/bag_debug.log", maxBytes=2 * 1024 * 1024)
```
- 操作类型:ADD、REMOVE、CLEAR、TRANSFER、SPLIT、MERGE
- 输出:文件,每 2 MB 滚动
## 14. 生命周期和插件入口
### 14.1 `base/plugin_entry.py`
base 侧入口会做这些事:
- 初始化检查
- 建表
- 创建 `BagManager`
- 退出时销毁 `BagManager`
### 14.2 `bots/plugin_entry.py`
bots 侧入口主要用于验证插件 common 模块可导入,方便机器人测试环境接入。
## 15. 常见使用场景
### 15.1 登录后显示背包
```python
def onClientEnabled(self):
self.bag.requestBagList()
```
### 15.2 发放奖励
```python
def giveReward(self, avatar, itemID, count):
bag = avatar.bag
bag.addItem(itemID, count, 1, "", opID="reward_202605", reason="REWARD")
```
### 15.3 邮件附件
```python
bag = receiverAvatar.bag
bag.addItem(2001, 1, 0, '{"quality":"rare"}', opID="mail_1001", reason="MAIL")
```
### 15.4 多物品交易
```python
bag.transferItems(
targetDBID,
[
{"bid": 101, "count": 1},
{"bid": 102, "count": 3},
],
opID="trade_9001",
reason="TRADE"
)
```
### 15.5 关闭某个回调
```python
avatar.bag.setCallbackSwitch("error", 0)
```
## 16. 建议和限制
### 建议
- 优先用服务端 API,不要让客户端直接碰高危写接口。
- 发奖励、扣物品、交易、邮件附件,最好都带 `opID`。
- 需要稳定追踪时,把 `reason` 和 `context` 填上。
- 容量、回调开关最好用 `setCapacity()`、`setCallbackSwitch()` 统一设置。
### 限制
- `transferItems()` 目前不是同连接事务版。
- 绑定、过期、锁定字段已经有了,但业务规则还需要你自己定义。
- 客户端回调开关只是“是否通知”,不是“是否执行操作”。
- `capacity` 现在按实例条数统计,不是按重量、体积或 stack 数量统计。
## 17. 一个完整例子
```python
def onAvatarReady(avatar):
bag = avatar.bag
bag.setCapacity(120)
bag.setCallbackSwitch("page", 1)
bag.setCallbackSwitch("updated", 1)
bag.addItem(1001, 3, 1, "", opID="login_bonus", reason="LOGIN")
bag.addItem(2001, 1, 0, '{"atk": 12, "quality": "rare"}', opID="gift_01", reason="GIFT")
bag.requestBagList()
```
这就是最常见的接法:先定容量,再发物品,再刷新列表。
## 18. 测试
测试代码在 `common/test/` 下,分两层:
### 18.1 纯函数测试(pytest,不依赖 KBE)
```bash
cd plugins/Bag/common/test
pytest test_bag_model.py test_bag_storage.py -v
```
| 文件 | 覆盖范围 |
|------|---------|
| `test_bag_model.py` | 数据规整:normalize_*、make_item、empty_item、page_items 等 12 个函数 |
| `test_bag_storage.py` | SQL 生成:create_table_sql、insert_op_log_sql、escape_sql_text、decode_* 等 |
### 18.2 KBE 集成测试(baseapp 运行时)
在 `base/plugin_entry.py` 的 `onComponentReady` 末尾加入:
```python
from plugins.Bag.common.test.BagServiceTest import start
start()
```
测试流程(19 步回调链):
1. 建表 → 2. 设置容量 → 3. 读取容量 → 4-5. 添加两种物品 → 6. 全量查询 → 7. 分页查询 → 8. 单物品查询 → 9. 数量统计 → 10. 拆分 → 11. 合并 → 12. 交换 → 13. 移动 → 14. 整理 → 15. 删除 → 16. 跨玩家转移 → 17. 清空 → 18. 验证空背包 → 19. 清理目标背包
测试使用专用 DBID `99999901` / `99999902`,不会污染真实玩家数据。每一步失败会打 `ERROR_MSG` 并终止。