koishi-plugin-eqserver-connect 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.d.ts +15 -0
- package/lib/index.js +314 -0
- package/package.json +26 -0
- package/readme.md +20 -0
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Context, Schema } from 'koishi';
|
|
2
|
+
export declare const name = "eqserver-connect";
|
|
3
|
+
export interface Config {
|
|
4
|
+
host: string;
|
|
5
|
+
port: number;
|
|
6
|
+
user: string;
|
|
7
|
+
password: string;
|
|
8
|
+
database: string;
|
|
9
|
+
groupIds: string[];
|
|
10
|
+
timezoneOffset: number;
|
|
11
|
+
minReward: number;
|
|
12
|
+
maxReward: number;
|
|
13
|
+
}
|
|
14
|
+
export declare const Config: Schema<Config>;
|
|
15
|
+
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __export = (target, all) => {
|
|
6
|
+
for (var name2 in all)
|
|
7
|
+
__defProp(target, name2, { get: all[name2], enumerable: true });
|
|
8
|
+
};
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
|
+
var src_exports = {};
|
|
19
|
+
__export(src_exports, {
|
|
20
|
+
Config: () => Config,
|
|
21
|
+
apply: () => apply,
|
|
22
|
+
name: () => name
|
|
23
|
+
});
|
|
24
|
+
module.exports = __toCommonJS(src_exports);
|
|
25
|
+
var import_koishi = require("koishi");
|
|
26
|
+
var import_pg = require("pg");
|
|
27
|
+
const name = "eqserver-connect";
|
|
28
|
+
const Config = import_koishi.Schema.object({
|
|
29
|
+
host: import_koishi.Schema.string().default("127.0.0.1").description("PostgreSQL IP"),
|
|
30
|
+
port: import_koishi.Schema.number().default(5432).description("PostgreSQL \u7AEF\u53E3"),
|
|
31
|
+
user: import_koishi.Schema.string().default("postgres").description("PostgreSQL \u8D26\u53F7"),
|
|
32
|
+
password: import_koishi.Schema.string().role("secret").default("").description("PostgreSQL \u5BC6\u7801"),
|
|
33
|
+
database: import_koishi.Schema.string().default("postgres").description("\u6570\u636E\u5E93\u540D"),
|
|
34
|
+
groupIds: import_koishi.Schema.array(import_koishi.Schema.string()).role("table").default([]).description("\u5141\u8BB8\u7B7E\u5230\u7684\u7FA4\u804A ID \u5217\u8868\uFF0C\u53EF\u586B channelId \u6216 platform:channelId\u3002sandbox \u7FA4\u804A\u9ED8\u8BA4\u5141\u8BB8\u3002"),
|
|
35
|
+
timezoneOffset: import_koishi.Schema.number().default(8).description("\u7B7E\u5230\u65F6\u533A\u504F\u79FB\uFF08\u5C0F\u65F6\uFF09\uFF0C\u9ED8\u8BA4 +8\u3002"),
|
|
36
|
+
minReward: import_koishi.Schema.number().default(100).description("\u7B7E\u5230\u6700\u5C0F\u5956\u52B1\u3002"),
|
|
37
|
+
maxReward: import_koishi.Schema.number().default(500).description("\u7B7E\u5230\u6700\u5927\u5956\u52B1\u3002")
|
|
38
|
+
});
|
|
39
|
+
const HOUR_MS = 60 * 60 * 1e3;
|
|
40
|
+
function getSignDay(now, timezoneOffset) {
|
|
41
|
+
const shifted = now.getTime() + timezoneOffset * HOUR_MS - 12 * HOUR_MS;
|
|
42
|
+
return new Date(shifted).toISOString().slice(0, 10);
|
|
43
|
+
}
|
|
44
|
+
function normalizeGroupIds(groupIds) {
|
|
45
|
+
if (!Array.isArray(groupIds)) return [];
|
|
46
|
+
return [...new Set(
|
|
47
|
+
groupIds.map((item) => typeof item === "string" ? item.trim() : "").filter(Boolean)
|
|
48
|
+
)];
|
|
49
|
+
}
|
|
50
|
+
function isSandboxGroupSession(session) {
|
|
51
|
+
const platform = session.platform || "";
|
|
52
|
+
if (!platform.startsWith("sandbox")) return false;
|
|
53
|
+
if (!session.channelId) return false;
|
|
54
|
+
if (session.isDirect) return false;
|
|
55
|
+
if (session.userId && session.channelId === `@${session.userId}`) return false;
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
function isAllowedGroup(session, groupIds) {
|
|
59
|
+
if (!session.channelId) return false;
|
|
60
|
+
if (isSandboxGroupSession(session)) return true;
|
|
61
|
+
if (!groupIds.length) return false;
|
|
62
|
+
const plainId = session.channelId;
|
|
63
|
+
const scopedId = `${session.platform}:${session.channelId}`;
|
|
64
|
+
return groupIds.includes(plainId) || groupIds.includes(scopedId);
|
|
65
|
+
}
|
|
66
|
+
function getRewardRange(minReward, maxReward) {
|
|
67
|
+
const left = Math.max(0, Math.floor(Math.min(minReward, maxReward)));
|
|
68
|
+
const right = Math.max(left, Math.floor(Math.max(minReward, maxReward)));
|
|
69
|
+
return [left, right];
|
|
70
|
+
}
|
|
71
|
+
function randomInt(min, max) {
|
|
72
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
73
|
+
}
|
|
74
|
+
function isUniqueViolation(error) {
|
|
75
|
+
if (!error || typeof error !== "object") return false;
|
|
76
|
+
return error.code === "23505";
|
|
77
|
+
}
|
|
78
|
+
async function getStorePlayersColumnType(client, columnName) {
|
|
79
|
+
const result = await client.query(
|
|
80
|
+
`
|
|
81
|
+
SELECT format_type(a.atttypid, a.atttypmod) AS column_type
|
|
82
|
+
FROM pg_attribute a
|
|
83
|
+
JOIN pg_class c ON a.attrelid = c.oid
|
|
84
|
+
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
85
|
+
WHERE c.relname = 'store_players'
|
|
86
|
+
AND n.nspname = ANY(current_schemas(false))
|
|
87
|
+
AND a.attname = $1
|
|
88
|
+
AND a.attnum > 0
|
|
89
|
+
AND NOT a.attisdropped
|
|
90
|
+
LIMIT 1
|
|
91
|
+
`,
|
|
92
|
+
[columnName]
|
|
93
|
+
);
|
|
94
|
+
if (!result.rowCount) {
|
|
95
|
+
throw new Error(`\u7F3A\u5C11\u5B57\u6BB5\uFF1Astore_players.${columnName}`);
|
|
96
|
+
}
|
|
97
|
+
return result.rows[0].column_type;
|
|
98
|
+
}
|
|
99
|
+
function apply(ctx, config) {
|
|
100
|
+
const logger = ctx.logger(name);
|
|
101
|
+
const pool = new import_pg.Pool({
|
|
102
|
+
host: config.host,
|
|
103
|
+
port: config.port,
|
|
104
|
+
user: config.user,
|
|
105
|
+
password: config.password,
|
|
106
|
+
database: config.database
|
|
107
|
+
});
|
|
108
|
+
const allowedGroupIds = normalizeGroupIds(config.groupIds);
|
|
109
|
+
const [minReward, maxReward] = getRewardRange(config.minReward, config.maxReward);
|
|
110
|
+
let initialized = false;
|
|
111
|
+
let initializeTask = null;
|
|
112
|
+
async function initDatabase() {
|
|
113
|
+
if (initialized) return;
|
|
114
|
+
if (initializeTask) return initializeTask;
|
|
115
|
+
initializeTask = (async () => {
|
|
116
|
+
const playerIdType = await getStorePlayersColumnType(pool, "id");
|
|
117
|
+
await getStorePlayersColumnType(pool, "authid");
|
|
118
|
+
await getStorePlayersColumnType(pool, "credits");
|
|
119
|
+
await pool.query(`
|
|
120
|
+
CREATE TABLE IF NOT EXISTS koishi_player_bind (
|
|
121
|
+
id BIGSERIAL PRIMARY KEY,
|
|
122
|
+
koishi_platform TEXT NOT NULL,
|
|
123
|
+
koishi_user_id TEXT NOT NULL,
|
|
124
|
+
player_id ${playerIdType} NOT NULL REFERENCES store_players(id) ON DELETE CASCADE,
|
|
125
|
+
authid TEXT NOT NULL UNIQUE,
|
|
126
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
127
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
128
|
+
UNIQUE (koishi_platform, koishi_user_id)
|
|
129
|
+
)
|
|
130
|
+
`);
|
|
131
|
+
await pool.query(`
|
|
132
|
+
CREATE TABLE IF NOT EXISTS koishi_signin (
|
|
133
|
+
id BIGSERIAL PRIMARY KEY,
|
|
134
|
+
player_id ${playerIdType} NOT NULL REFERENCES store_players(id) ON DELETE CASCADE,
|
|
135
|
+
koishi_platform TEXT NOT NULL,
|
|
136
|
+
koishi_user_id TEXT NOT NULL,
|
|
137
|
+
sign_day DATE NOT NULL,
|
|
138
|
+
reward INTEGER NOT NULL,
|
|
139
|
+
signed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
140
|
+
UNIQUE (player_id, sign_day)
|
|
141
|
+
)
|
|
142
|
+
`);
|
|
143
|
+
await pool.query(`
|
|
144
|
+
CREATE INDEX IF NOT EXISTS idx_koishi_signin_player_signed_at
|
|
145
|
+
ON koishi_signin (player_id, signed_at DESC)
|
|
146
|
+
`);
|
|
147
|
+
initialized = true;
|
|
148
|
+
logger.info("PostgreSQL connected and tables are ready.");
|
|
149
|
+
})();
|
|
150
|
+
try {
|
|
151
|
+
await initializeTask;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
logger.error(error);
|
|
154
|
+
throw error;
|
|
155
|
+
} finally {
|
|
156
|
+
initializeTask = null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function ensureDatabaseReady() {
|
|
160
|
+
try {
|
|
161
|
+
await initDatabase();
|
|
162
|
+
return true;
|
|
163
|
+
} catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
ctx.on("ready", () => {
|
|
168
|
+
void initDatabase().catch((error) => {
|
|
169
|
+
logger.warn("init failed, commands will retry on demand.");
|
|
170
|
+
logger.error(error);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
ctx.on("dispose", () => {
|
|
174
|
+
void pool.end().catch((error) => logger.warn(String(error)));
|
|
175
|
+
});
|
|
176
|
+
ctx.command("register <authid:text>", "\u7ED1\u5B9A\u6069\u60C5\u670D\u52A1\u5668\u73A9\u5BB6").alias("\u6CE8\u518C").action(async ({ session }, authid) => {
|
|
177
|
+
if (!session?.userId) return "\u65E0\u6CD5\u8BC6\u522B Koishi \u7528\u6237\u3002";
|
|
178
|
+
if (!authid?.trim()) return "\u7528\u6CD5\uFF1A\u6CE8\u518C [steamid]\uFF0C\u5728\u6E38\u620F\u5185\u8F93\u5165!steamid\u83B7\u53D6\u4F60\u81EA\u5DF1\u7684steamid";
|
|
179
|
+
const authidText = authid.trim();
|
|
180
|
+
if (!/^\d+:\d+$/.test(authidText)) {
|
|
181
|
+
return "steamid \u683C\u5F0F\u9519\u8BEF\uFF0C\u793A\u4F8B\uFF1A\u6CE8\u518C 1:99999999\uFF0C\u5728\u6E38\u620F\u5185\u8F93\u5165!steamid\u83B7\u53D6\u4F60\u81EA\u5DF1\u7684steamid";
|
|
182
|
+
}
|
|
183
|
+
if (!await ensureDatabaseReady()) {
|
|
184
|
+
return "\u6570\u636E\u5E93\u8FDE\u63A5\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u63D2\u4EF6\u8BBE\u7F6E\u3002";
|
|
185
|
+
}
|
|
186
|
+
const platform = session.platform || "unknown";
|
|
187
|
+
const userId = session.userId;
|
|
188
|
+
const client = await pool.connect();
|
|
189
|
+
try {
|
|
190
|
+
const playerResult = await client.query(
|
|
191
|
+
`
|
|
192
|
+
SELECT id
|
|
193
|
+
FROM store_players
|
|
194
|
+
WHERE authid = $1
|
|
195
|
+
LIMIT 1
|
|
196
|
+
`,
|
|
197
|
+
[authidText]
|
|
198
|
+
);
|
|
199
|
+
if (!playerResult.rowCount) {
|
|
200
|
+
return `\u672A\u627E\u5230 authid=${authidText} \u5BF9\u5E94\u7684\u73A9\u5BB6\u3002`;
|
|
201
|
+
}
|
|
202
|
+
const playerId = playerResult.rows[0].id;
|
|
203
|
+
const occupied = await client.query(
|
|
204
|
+
`
|
|
205
|
+
SELECT koishi_platform, koishi_user_id
|
|
206
|
+
FROM koishi_player_bind
|
|
207
|
+
WHERE authid = $1
|
|
208
|
+
LIMIT 1
|
|
209
|
+
`,
|
|
210
|
+
[authidText]
|
|
211
|
+
);
|
|
212
|
+
if (occupied.rowCount && (occupied.rows[0].koishi_platform !== platform || occupied.rows[0].koishi_user_id !== userId)) {
|
|
213
|
+
return "\u8BE5 authid \u5DF2\u88AB\u5176\u4ED6 Koishi \u8D26\u53F7\u7ED1\u5B9A\u3002";
|
|
214
|
+
}
|
|
215
|
+
await client.query(
|
|
216
|
+
`
|
|
217
|
+
INSERT INTO koishi_player_bind (koishi_platform, koishi_user_id, player_id, authid)
|
|
218
|
+
VALUES ($1, $2, $3, $4)
|
|
219
|
+
ON CONFLICT (koishi_platform, koishi_user_id)
|
|
220
|
+
DO UPDATE SET
|
|
221
|
+
player_id = EXCLUDED.player_id,
|
|
222
|
+
authid = EXCLUDED.authid,
|
|
223
|
+
updated_at = NOW()
|
|
224
|
+
`,
|
|
225
|
+
[platform, userId, playerId, authidText]
|
|
226
|
+
);
|
|
227
|
+
return `\u6CE8\u518C\u6210\u529F\uFF0C\u5DF2\u7ED1\u5B9A\u73A9\u5BB6 ID\uFF1A${String(playerId)}`;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
logger.error(error);
|
|
230
|
+
return "\u6CE8\u518C\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
|
|
231
|
+
} finally {
|
|
232
|
+
client.release();
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
ctx.command("qd", "\u6BCF\u65E5\u7B7E\u5230").alias("\u7B7E\u5230").action(async ({ session }) => {
|
|
236
|
+
if (!session?.userId) return "\u65E0\u6CD5\u8BC6\u522B Koishi \u7528\u6237\u3002";
|
|
237
|
+
if (!isAllowedGroup(session, allowedGroupIds)) return;
|
|
238
|
+
if (!await ensureDatabaseReady()) {
|
|
239
|
+
return "\u6570\u636E\u5E93\u8FDE\u63A5\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u63D2\u4EF6\u8BBE\u7F6E\u3002";
|
|
240
|
+
}
|
|
241
|
+
const platform = session.platform || "unknown";
|
|
242
|
+
const userId = session.userId;
|
|
243
|
+
const bindResult = await pool.query(
|
|
244
|
+
`
|
|
245
|
+
SELECT player_id, authid
|
|
246
|
+
FROM koishi_player_bind
|
|
247
|
+
WHERE koishi_platform = $1
|
|
248
|
+
AND koishi_user_id = $2
|
|
249
|
+
LIMIT 1
|
|
250
|
+
`,
|
|
251
|
+
[platform, userId]
|
|
252
|
+
);
|
|
253
|
+
if (!bindResult.rowCount) {
|
|
254
|
+
return "\u4F60\u8FD8\u6CA1\u6709\u6CE8\u518C\uFF0C\u8BF7\u5148\u53D1\u9001\uFF1A\u6CE8\u518C [steamid]\uFF0C\u5728\u6E38\u620F\u5185\u8F93\u5165!steamid\u83B7\u53D6\u4F60\u81EA\u5DF1\u7684steamid";
|
|
255
|
+
}
|
|
256
|
+
const bind = bindResult.rows[0];
|
|
257
|
+
const signDay = getSignDay(/* @__PURE__ */ new Date(), config.timezoneOffset);
|
|
258
|
+
const reward = randomInt(minReward, maxReward);
|
|
259
|
+
const client = await pool.connect();
|
|
260
|
+
try {
|
|
261
|
+
await client.query("BEGIN");
|
|
262
|
+
const existed = await client.query(
|
|
263
|
+
`
|
|
264
|
+
SELECT reward
|
|
265
|
+
FROM koishi_signin
|
|
266
|
+
WHERE player_id = $1
|
|
267
|
+
AND sign_day = $2
|
|
268
|
+
LIMIT 1
|
|
269
|
+
`,
|
|
270
|
+
[bind.player_id, signDay]
|
|
271
|
+
);
|
|
272
|
+
if (existed.rowCount) {
|
|
273
|
+
await client.query("ROLLBACK");
|
|
274
|
+
return `\u4ECA\u5929\u5DF2\u7ECF\u7B7E\u5230\u8FC7\u4E86\uFF0C\u4E0A\u6B21\u83B7\u5F97 ${existed.rows[0].reward} \u6069\u60C5\u5E01\u3002`;
|
|
275
|
+
}
|
|
276
|
+
await client.query(
|
|
277
|
+
`
|
|
278
|
+
INSERT INTO koishi_signin (player_id, koishi_platform, koishi_user_id, sign_day, reward)
|
|
279
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
280
|
+
`,
|
|
281
|
+
[bind.player_id, platform, userId, signDay, reward]
|
|
282
|
+
);
|
|
283
|
+
const creditResult = await client.query(
|
|
284
|
+
`
|
|
285
|
+
UPDATE store_players
|
|
286
|
+
SET credits = COALESCE(credits, 0) + $1
|
|
287
|
+
WHERE id = $2
|
|
288
|
+
RETURNING credits
|
|
289
|
+
`,
|
|
290
|
+
[reward, bind.player_id]
|
|
291
|
+
);
|
|
292
|
+
if (!creditResult.rowCount) {
|
|
293
|
+
throw new Error(`\u672A\u627E\u5230\u73A9\u5BB6 ID\uFF1A${String(bind.player_id)}`);
|
|
294
|
+
}
|
|
295
|
+
await client.query("COMMIT");
|
|
296
|
+
return `\u7B7E\u5230\u6210\u529F\uFF0C\u83B7\u5F97 ${reward} \u6069\u60C5\u5E01\u3002\u5F53\u524D \u6069\u60C5\u5E01\uFF1A${String(creditResult.rows[0].credits)}`;
|
|
297
|
+
} catch (error) {
|
|
298
|
+
await client.query("ROLLBACK").catch(() => null);
|
|
299
|
+
if (isUniqueViolation(error)) {
|
|
300
|
+
return "\u4ECA\u5929\u5DF2\u7ECF\u7B7E\u5230\u8FC7\u4E86\u3002";
|
|
301
|
+
}
|
|
302
|
+
logger.error(error);
|
|
303
|
+
return "\u7B7E\u5230\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002";
|
|
304
|
+
} finally {
|
|
305
|
+
client.release();
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
310
|
+
0 && (module.exports = {
|
|
311
|
+
Config,
|
|
312
|
+
apply,
|
|
313
|
+
name
|
|
314
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "koishi-plugin-eqserver-connect",
|
|
3
|
+
"description": "EQ server PostgreSQL connector with register + daily signin",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"typings": "lib/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib",
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"scripts": {},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"chatbot",
|
|
15
|
+
"koishi",
|
|
16
|
+
"plugin",
|
|
17
|
+
"postgresql",
|
|
18
|
+
"signin"
|
|
19
|
+
],
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"koishi": "^4.18.0"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"pg": "^8.13.1"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# koishi-plugin-eqserver-connect
|
|
2
|
+
|
|
3
|
+
PostgreSQL connector plugin for Koishi:
|
|
4
|
+
|
|
5
|
+
- Register player mapping with `注册 <authid>` (alias: `register`).
|
|
6
|
+
- Daily sign-in with `签到` (alias: `qd`).
|
|
7
|
+
- One sign-in per day, reset at 12:00 (configurable timezone offset).
|
|
8
|
+
- Rewards `100-500` by default, and directly adds to `store_players.credits`.
|
|
9
|
+
- Group whitelist in plugin config (`groupIds`), with sandbox groups allowed by default.
|
|
10
|
+
|
|
11
|
+
## Required database table
|
|
12
|
+
|
|
13
|
+
The plugin expects an existing table:
|
|
14
|
+
|
|
15
|
+
- `store_players(id, authid, credits, ...)`
|
|
16
|
+
|
|
17
|
+
It auto-creates:
|
|
18
|
+
|
|
19
|
+
- `koishi_player_bind` (Koishi user -> player binding)
|
|
20
|
+
- `koishi_signin` (daily sign-in records)
|