koishi-plugin-ggcevo 1.0.4 → 1.0.6
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 +37 -0
- package/lib/index.js +332 -1
- package/package.json +1 -1
- package/readme.md +62 -0
package/lib/index.d.ts
CHANGED
|
@@ -5,6 +5,13 @@ export interface Config {
|
|
|
5
5
|
mapMonitorGroups: string[];
|
|
6
6
|
mapMonitorMapIds: number[];
|
|
7
7
|
mapMonitorApiUrl: string;
|
|
8
|
+
tencentDocsEnabled: boolean;
|
|
9
|
+
tencentDocsClientId: string;
|
|
10
|
+
tencentDocsClientSecret: string;
|
|
11
|
+
tencentDocsAccessToken: string;
|
|
12
|
+
tencentDocsOpenId: string;
|
|
13
|
+
tencentDocsBanListFileId: string;
|
|
14
|
+
tencentDocsBanListSheetId: string;
|
|
8
15
|
}
|
|
9
16
|
export declare const Config: Schema<Config>;
|
|
10
17
|
export declare const inject: {
|
|
@@ -33,6 +40,8 @@ declare module 'koishi' {
|
|
|
33
40
|
ggcevo_exchange_log: ExchangeLog;
|
|
34
41
|
ggcevo_activity: Activity;
|
|
35
42
|
ggcevo_activity_claim_log: ActivityClaimLog;
|
|
43
|
+
ggcevo_docs_token: GgcEvoDocsToken;
|
|
44
|
+
ggcevo_ban_record: GgcEvoBanRecord;
|
|
36
45
|
}
|
|
37
46
|
}
|
|
38
47
|
export interface GgcEvoPlayer {
|
|
@@ -49,6 +58,34 @@ export interface GgcEvoMapMonitorState {
|
|
|
49
58
|
lastState: string;
|
|
50
59
|
lastCheckedAt: Date;
|
|
51
60
|
}
|
|
61
|
+
export interface GgcEvoDocsToken {
|
|
62
|
+
/** 账号标识, 应用级账号固定为 'app_account' */
|
|
63
|
+
user_id: string;
|
|
64
|
+
/** 应用级账号 Open ID */
|
|
65
|
+
docs_user_id: string;
|
|
66
|
+
access_token: string;
|
|
67
|
+
refresh_token: string;
|
|
68
|
+
expires_at: Date;
|
|
69
|
+
update_time: Date;
|
|
70
|
+
}
|
|
71
|
+
export interface GgcEvoBanRecord {
|
|
72
|
+
/** 自增ID, 对应文档行号(行号 = id + 1, 跳过表头) */
|
|
73
|
+
id: number;
|
|
74
|
+
/** A列: 游戏句柄 */
|
|
75
|
+
handle: string;
|
|
76
|
+
/** C列: 封禁等级 */
|
|
77
|
+
ban_level: string;
|
|
78
|
+
/** D列: 处罚原因 (text 类型, 支持长文本) */
|
|
79
|
+
reason: string;
|
|
80
|
+
/** E列: 处罚次数 */
|
|
81
|
+
count: string;
|
|
82
|
+
/** F列: 审核员 */
|
|
83
|
+
auditor: string;
|
|
84
|
+
/** G列: 审核时间 */
|
|
85
|
+
audit_time: string;
|
|
86
|
+
/** 最近一次同步时间 */
|
|
87
|
+
update_time: Date;
|
|
88
|
+
}
|
|
52
89
|
export interface Backpack {
|
|
53
90
|
id: number;
|
|
54
91
|
user_id: string;
|
package/lib/index.js
CHANGED
|
@@ -35,7 +35,14 @@ var Config = import_koishi.Schema.object({
|
|
|
35
35
|
mapMonitorEnabled: import_koishi.Schema.boolean().description("是否启用地图检测定时任务").default(false),
|
|
36
36
|
mapMonitorGroups: import_koishi.Schema.array(import_koishi.Schema.string()).description("地图检测广播的群组ID列表").default([]),
|
|
37
37
|
mapMonitorMapIds: import_koishi.Schema.array(import_koishi.Schema.number()).description("需要检测的地图ID列表").default([]),
|
|
38
|
-
mapMonitorApiUrl: import_koishi.Schema.string().description("地图检测API地址").default("https://server.dreamprotocol.info:13085/mapmonitor/maps")
|
|
38
|
+
mapMonitorApiUrl: import_koishi.Schema.string().description("地图检测API地址").default("https://server.dreamprotocol.info:13085/mapmonitor/maps"),
|
|
39
|
+
tencentDocsEnabled: import_koishi.Schema.boolean().description("是否启用腾讯文档功能").default(false),
|
|
40
|
+
tencentDocsClientId: import_koishi.Schema.string().description("腾讯文档开放平台应用 Client ID(应用ID)").default(""),
|
|
41
|
+
tencentDocsClientSecret: import_koishi.Schema.string().description("腾讯文档应用 Client Secret(应用级账号模式需要, 用户级模式留空)").role("secret").default(""),
|
|
42
|
+
tencentDocsAccessToken: import_koishi.Schema.string().description("腾讯文档 Access Token(用户级模式必填, 通过扫码授权获取)").role("secret").default(""),
|
|
43
|
+
tencentDocsOpenId: import_koishi.Schema.string().description("腾讯文档 Open ID(用户级模式必填, 与 Access Token 同时获取)").default(""),
|
|
44
|
+
tencentDocsBanListFileId: import_koishi.Schema.string().description("封禁记录在线表格的文件ID(短ID或完整ID)").default("DTVdYZVBDdFhEUkp6"),
|
|
45
|
+
tencentDocsBanListSheetId: import_koishi.Schema.string().description("封禁记录工作表ID(表格URL中tab参数)").default("BB08J2")
|
|
39
46
|
});
|
|
40
47
|
var inject = {
|
|
41
48
|
required: ["database"]
|
|
@@ -183,6 +190,29 @@ function apply(ctx, config) {
|
|
|
183
190
|
}, {
|
|
184
191
|
primary: ["activity_id", "user_id"]
|
|
185
192
|
});
|
|
193
|
+
ctx.model.extend("ggcevo_docs_token", {
|
|
194
|
+
user_id: "string",
|
|
195
|
+
docs_user_id: "string",
|
|
196
|
+
access_token: "string",
|
|
197
|
+
refresh_token: "string",
|
|
198
|
+
expires_at: "timestamp",
|
|
199
|
+
update_time: "timestamp"
|
|
200
|
+
}, {
|
|
201
|
+
primary: "user_id"
|
|
202
|
+
});
|
|
203
|
+
ctx.model.extend("ggcevo_ban_record", {
|
|
204
|
+
id: "unsigned",
|
|
205
|
+
handle: "string",
|
|
206
|
+
ban_level: "string",
|
|
207
|
+
reason: "text",
|
|
208
|
+
count: "string",
|
|
209
|
+
auditor: "string",
|
|
210
|
+
audit_time: "string",
|
|
211
|
+
update_time: "timestamp"
|
|
212
|
+
}, {
|
|
213
|
+
primary: "id",
|
|
214
|
+
autoInc: true
|
|
215
|
+
});
|
|
186
216
|
if (config.mapMonitorEnabled && config.mapMonitorGroups.length > 0 && config.mapMonitorMapIds.length > 0) {
|
|
187
217
|
ctx.setInterval(async () => {
|
|
188
218
|
try {
|
|
@@ -1917,6 +1947,307 @@ ${message}
|
|
|
1917
1947
|
});
|
|
1918
1948
|
return `✅ 赎罪券使用成功!`;
|
|
1919
1949
|
});
|
|
1950
|
+
const DOCS_APP_ACCOUNT_KEY = "app_account";
|
|
1951
|
+
const isUserTokenMode = /* @__PURE__ */ __name(() => !!config.tencentDocsAccessToken && !!config.tencentDocsOpenId, "isUserTokenMode");
|
|
1952
|
+
const isDocsConfigured = /* @__PURE__ */ __name(() => config.tencentDocsEnabled && !!config.tencentDocsClientId && (isUserTokenMode() || !!config.tencentDocsClientSecret), "isDocsConfigured");
|
|
1953
|
+
const docsOAuth = {
|
|
1954
|
+
/** 获取应用级账号 Access Token / Refresh Token (应用级账号为应用全局唯一, 无需用户授权) */
|
|
1955
|
+
async getAppAccountToken() {
|
|
1956
|
+
return ctx.http.get("https://docs.qq.com/oauth/v2/app-account-token", {
|
|
1957
|
+
params: {
|
|
1958
|
+
client_id: config.tencentDocsClientId,
|
|
1959
|
+
client_secret: config.tencentDocsClientSecret
|
|
1960
|
+
}
|
|
1961
|
+
});
|
|
1962
|
+
},
|
|
1963
|
+
/** 使用 Refresh Token 刷新 Access Token (Refresh Token 有效期 1 年, 回包不含新 Refresh Token) */
|
|
1964
|
+
async refreshToken(refreshToken) {
|
|
1965
|
+
return ctx.http.get("https://docs.qq.com/oauth/v2/token", {
|
|
1966
|
+
params: {
|
|
1967
|
+
client_id: config.tencentDocsClientId,
|
|
1968
|
+
client_secret: config.tencentDocsClientSecret,
|
|
1969
|
+
grant_type: "refresh_token",
|
|
1970
|
+
refresh_token: refreshToken
|
|
1971
|
+
}
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1974
|
+
};
|
|
1975
|
+
const getValidDocsToken = /* @__PURE__ */ __name(async () => {
|
|
1976
|
+
if (isUserTokenMode()) {
|
|
1977
|
+
return {
|
|
1978
|
+
user_id: DOCS_APP_ACCOUNT_KEY,
|
|
1979
|
+
docs_user_id: config.tencentDocsOpenId,
|
|
1980
|
+
access_token: config.tencentDocsAccessToken,
|
|
1981
|
+
refresh_token: "",
|
|
1982
|
+
expires_at: new Date(Date.now() + 365 * 24 * 60 * 60 * 1e3),
|
|
1983
|
+
// 用户级模式无法自动判断过期, 设为1年后
|
|
1984
|
+
update_time: /* @__PURE__ */ new Date()
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
const [record] = await ctx.database.get("ggcevo_docs_token", { user_id: DOCS_APP_ACCOUNT_KEY });
|
|
1988
|
+
if (record && record.expires_at.getTime() > Date.now() + 5 * 60 * 1e3) return record;
|
|
1989
|
+
const formatHttpError = /* @__PURE__ */ __name((e) => {
|
|
1990
|
+
const parts = [];
|
|
1991
|
+
if (e?.name) parts.push(e.name);
|
|
1992
|
+
if (e?.message) parts.push(e.message);
|
|
1993
|
+
if (e?.response) {
|
|
1994
|
+
parts.push(`HTTP ${e.response.status}`);
|
|
1995
|
+
const body = e.response.data;
|
|
1996
|
+
if (body != null) parts.push(typeof body === "string" ? body : JSON.stringify(body));
|
|
1997
|
+
} else if (e?.code) {
|
|
1998
|
+
parts.push(`code=${e.code}`);
|
|
1999
|
+
}
|
|
2000
|
+
return parts.join(" | ") || String(e);
|
|
2001
|
+
}, "formatHttpError");
|
|
2002
|
+
try {
|
|
2003
|
+
let resp;
|
|
2004
|
+
if (record) {
|
|
2005
|
+
resp = await docsOAuth.refreshToken(record.refresh_token).catch((refreshErr) => {
|
|
2006
|
+
ctx.logger("ggcevo").warn("刷新 Token 失败, 回退到重新获取应用级 Token: %s", formatHttpError(refreshErr));
|
|
2007
|
+
return docsOAuth.getAppAccountToken();
|
|
2008
|
+
});
|
|
2009
|
+
} else {
|
|
2010
|
+
resp = await docsOAuth.getAppAccountToken();
|
|
2011
|
+
}
|
|
2012
|
+
if (!resp?.access_token) {
|
|
2013
|
+
ctx.logger("ggcevo").warn("应用级 Token 回包缺少 access_token: %o", resp);
|
|
2014
|
+
return record || null;
|
|
2015
|
+
}
|
|
2016
|
+
await ctx.database.upsert("ggcevo_docs_token", [{
|
|
2017
|
+
user_id: DOCS_APP_ACCOUNT_KEY,
|
|
2018
|
+
docs_user_id: resp.user_id || record?.docs_user_id || "",
|
|
2019
|
+
access_token: resp.access_token,
|
|
2020
|
+
refresh_token: resp.refresh_token || record?.refresh_token || "",
|
|
2021
|
+
expires_at: new Date(Date.now() + (resp.expires_in || 0) * 1e3),
|
|
2022
|
+
update_time: /* @__PURE__ */ new Date()
|
|
2023
|
+
}]);
|
|
2024
|
+
const [updated] = await ctx.database.get("ggcevo_docs_token", { user_id: DOCS_APP_ACCOUNT_KEY });
|
|
2025
|
+
return updated || null;
|
|
2026
|
+
} catch (e) {
|
|
2027
|
+
ctx.logger("ggcevo").warn("获取/刷新腾讯文档应用级 Token 失败: %s", formatHttpError(e));
|
|
2028
|
+
return record || null;
|
|
2029
|
+
}
|
|
2030
|
+
}, "getValidDocsToken");
|
|
2031
|
+
const docsApiRequest = /* @__PURE__ */ __name(async (method, path, data) => {
|
|
2032
|
+
const token = await getValidDocsToken();
|
|
2033
|
+
if (!token) throw new Error("腾讯文档应用级账号不可用, 请检查插件配置(Client ID / Client Secret)");
|
|
2034
|
+
const url = `https://docs.qq.com/openapi${path}`;
|
|
2035
|
+
const headers = {
|
|
2036
|
+
"Access-Token": token.access_token,
|
|
2037
|
+
"Client-Id": config.tencentDocsClientId,
|
|
2038
|
+
"Open-Id": token.docs_user_id
|
|
2039
|
+
};
|
|
2040
|
+
if (method === "GET" || method === "DELETE") {
|
|
2041
|
+
return ctx.http.get(url, { headers, params: data });
|
|
2042
|
+
}
|
|
2043
|
+
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
2044
|
+
const form = new URLSearchParams(data || {});
|
|
2045
|
+
if (method === "PUT") return ctx.http.put(url, form, { headers });
|
|
2046
|
+
return ctx.http.post(url, form, { headers });
|
|
2047
|
+
}, "docsApiRequest");
|
|
2048
|
+
ctx.command("腾讯文档/状态", "查看腾讯文档授权状态").action(async () => {
|
|
2049
|
+
if (!isDocsConfigured()) return "❌ 腾讯文档功能未启用或配置不完整。";
|
|
2050
|
+
const token = await getValidDocsToken();
|
|
2051
|
+
if (!token) {
|
|
2052
|
+
return isUserTokenMode() ? "❌ 用户级 Token 模式: 请检查 Access Token / Open ID 配置。" : "❌ 应用级账号 Token 获取失败, 请检查 Client ID / Client Secret 配置。";
|
|
2053
|
+
}
|
|
2054
|
+
const mode = isUserTokenMode() ? "用户级(扫码授权)" : "应用级账号(自动刷新)";
|
|
2055
|
+
return [
|
|
2056
|
+
"📄 腾讯文档授权状态",
|
|
2057
|
+
`授权模式: ${mode}`,
|
|
2058
|
+
`Client ID: ${config.tencentDocsClientId}`,
|
|
2059
|
+
`Open ID: ${token.docs_user_id}`,
|
|
2060
|
+
`令牌过期时间: ${toBeijingTime(token.expires_at.toISOString())}`,
|
|
2061
|
+
`最近更新: ${toBeijingTime(token.update_time.toISOString())}`
|
|
2062
|
+
].join("\n");
|
|
2063
|
+
});
|
|
2064
|
+
const extractCellText = /* @__PURE__ */ __name((cell) => {
|
|
2065
|
+
if (!cell?.cellValue) return "";
|
|
2066
|
+
const v = cell.cellValue;
|
|
2067
|
+
if (v.text != null) return String(v.text);
|
|
2068
|
+
if (v.number != null) return String(v.number);
|
|
2069
|
+
if (v.time) {
|
|
2070
|
+
const t = v.time;
|
|
2071
|
+
const pad = /* @__PURE__ */ __name((n) => String(n ?? 0).padStart(2, "0"), "pad");
|
|
2072
|
+
const date = `${t.year ?? ""}-${pad(t.month)}-${pad(t.day)}`.replace(/^-+/, "");
|
|
2073
|
+
const time = `${pad(t.hour)}:${pad(t.minute)}:${pad(t.second)}`;
|
|
2074
|
+
return t.hour != null ? `${date} ${time}` : date;
|
|
2075
|
+
}
|
|
2076
|
+
if (v.link) return v.link.text || v.link.url || "";
|
|
2077
|
+
if (v.location) return v.location.name || "";
|
|
2078
|
+
return "";
|
|
2079
|
+
}, "extractCellText");
|
|
2080
|
+
const normalizeHandle = /* @__PURE__ */ __name((raw) => {
|
|
2081
|
+
const s = (raw || "").trim();
|
|
2082
|
+
const m = s.match(/^([1235])-s2-([12])-(\d+)$/i);
|
|
2083
|
+
if (m) return `${m[1]}-S2-${m[2]}-${m[3]}`;
|
|
2084
|
+
return s.toLowerCase();
|
|
2085
|
+
}, "normalizeHandle");
|
|
2086
|
+
const fetchBanRecordsFromDocs = /* @__PURE__ */ __name(async () => {
|
|
2087
|
+
const fileId = config.tencentDocsBanListFileId;
|
|
2088
|
+
const sheetId = config.tencentDocsBanListSheetId;
|
|
2089
|
+
const meta = await docsApiRequest("GET", `/spreadsheet/v3/files/${fileId}`, { concise: 1 });
|
|
2090
|
+
const metaOk = meta?.code === 0 || meta?.ret === 0;
|
|
2091
|
+
if (metaOk === false && meta?.code != null) {
|
|
2092
|
+
throw new Error(`查询工作表信息失败: ${meta.message || "未知错误"}(code=${meta.code})`);
|
|
2093
|
+
}
|
|
2094
|
+
const props = meta?.data?.properties || meta?.properties || [];
|
|
2095
|
+
const sheetInfo = props.find((s) => s.sheetId === sheetId);
|
|
2096
|
+
const rowTotal = Number(sheetInfo?.rowTotal) || 0;
|
|
2097
|
+
if (rowTotal <= 1) return [];
|
|
2098
|
+
const allRows = [];
|
|
2099
|
+
const batchSize = 1e3;
|
|
2100
|
+
for (let start = 2; start <= rowTotal; start += batchSize) {
|
|
2101
|
+
const end = Math.min(start + batchSize - 1, rowTotal);
|
|
2102
|
+
const resp = await docsApiRequest("GET", `/spreadsheet/v3/files/${fileId}/${sheetId}/A${start}:G${end}`);
|
|
2103
|
+
const respOk = resp?.code === 0 || resp?.ret === 0;
|
|
2104
|
+
if (respOk === false && resp?.code != null) {
|
|
2105
|
+
throw new Error(`查询范围 A${start}:G${end} 失败: ${resp.message || "未知错误"}(code=${resp.code})`);
|
|
2106
|
+
}
|
|
2107
|
+
const rows = resp?.data?.gridData?.rows || resp?.gridData?.rows || [];
|
|
2108
|
+
for (const row of rows) {
|
|
2109
|
+
const cells = row.values || [];
|
|
2110
|
+
allRows.push([
|
|
2111
|
+
extractCellText(cells[0]),
|
|
2112
|
+
// A: 句柄
|
|
2113
|
+
extractCellText(cells[1]),
|
|
2114
|
+
// B: (未使用)
|
|
2115
|
+
extractCellText(cells[2]),
|
|
2116
|
+
// C: 封禁等级
|
|
2117
|
+
extractCellText(cells[3]),
|
|
2118
|
+
// D: 处罚原因
|
|
2119
|
+
extractCellText(cells[4]),
|
|
2120
|
+
// E: 处罚次数
|
|
2121
|
+
extractCellText(cells[5]),
|
|
2122
|
+
// F: 审核员
|
|
2123
|
+
extractCellText(cells[6])
|
|
2124
|
+
// G: 审核时间
|
|
2125
|
+
]);
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
return allRows;
|
|
2129
|
+
}, "fetchBanRecordsFromDocs");
|
|
2130
|
+
const syncBanRecords = /* @__PURE__ */ __name(async () => {
|
|
2131
|
+
const rows = await fetchBanRecordsFromDocs();
|
|
2132
|
+
const now = /* @__PURE__ */ new Date();
|
|
2133
|
+
const records = rows.map((r, i) => ({
|
|
2134
|
+
id: i + 1,
|
|
2135
|
+
// 自增 id, 对应文档行号 (id=1 → 文档第2行)
|
|
2136
|
+
handle: r[0] || "",
|
|
2137
|
+
ban_level: r[2] || "",
|
|
2138
|
+
reason: r[3] || "",
|
|
2139
|
+
count: r[4] || "",
|
|
2140
|
+
auditor: r[5] || "",
|
|
2141
|
+
audit_time: r[6] || "",
|
|
2142
|
+
update_time: now
|
|
2143
|
+
}));
|
|
2144
|
+
const oldRecords = await ctx.database.get("ggcevo_ban_record", {}, { fields: ["id"] });
|
|
2145
|
+
const oldCount = oldRecords.length;
|
|
2146
|
+
await ctx.database.remove("ggcevo_ban_record", {});
|
|
2147
|
+
if (records.length > 0) {
|
|
2148
|
+
await ctx.database.upsert("ggcevo_ban_record", records);
|
|
2149
|
+
}
|
|
2150
|
+
const diff = records.length - oldCount;
|
|
2151
|
+
ctx.logger("ggcevo").info(
|
|
2152
|
+
"封禁记录全量同步完成: 旧 %d 条 → 新 %d 条 (%s%d)",
|
|
2153
|
+
oldCount,
|
|
2154
|
+
records.length,
|
|
2155
|
+
diff >= 0 ? "+" : "",
|
|
2156
|
+
diff
|
|
2157
|
+
);
|
|
2158
|
+
return records.length;
|
|
2159
|
+
}, "syncBanRecords");
|
|
2160
|
+
if (isDocsConfigured() && config.tencentDocsBanListFileId && config.tencentDocsBanListSheetId) {
|
|
2161
|
+
const syncTask = /* @__PURE__ */ __name(async () => {
|
|
2162
|
+
try {
|
|
2163
|
+
const count = await syncBanRecords();
|
|
2164
|
+
ctx.logger("ggcevo").info("封禁记录同步完成, 共 %d 条", count);
|
|
2165
|
+
} catch (e) {
|
|
2166
|
+
ctx.logger("ggcevo").warn("封禁记录定时同步失败: %o", e);
|
|
2167
|
+
}
|
|
2168
|
+
}, "syncTask");
|
|
2169
|
+
ctx.setTimeout(syncTask, 5e3);
|
|
2170
|
+
ctx.setInterval(syncTask, 60 * 60 * 1e3);
|
|
2171
|
+
}
|
|
2172
|
+
ctx.command("ggcevo/同步封禁记录", { authority: 3 }).action(async () => {
|
|
2173
|
+
if (!isDocsConfigured()) return "❌ 腾讯文档功能未启用或配置不完整。";
|
|
2174
|
+
try {
|
|
2175
|
+
const count = await syncBanRecords();
|
|
2176
|
+
return `✅ 封禁记录同步完成, 共 ${count} 条。`;
|
|
2177
|
+
} catch (e) {
|
|
2178
|
+
ctx.logger("ggcevo").warn("手动同步封禁记录失败: %o", e);
|
|
2179
|
+
return `❌ 同步失败: ${e?.message || e}`;
|
|
2180
|
+
}
|
|
2181
|
+
});
|
|
2182
|
+
ctx.command("ggcevo/封禁记录").action(async (argv) => {
|
|
2183
|
+
if (!isDocsConfigured()) return "❌ 腾讯文档功能未启用或配置不完整。";
|
|
2184
|
+
const session = argv.session;
|
|
2185
|
+
const handle = await getHandle(session);
|
|
2186
|
+
if (!handle) {
|
|
2187
|
+
return "🔒 需要先绑定游戏句柄。\n💡 使用 `绑定句柄` 命令进行绑定。";
|
|
2188
|
+
}
|
|
2189
|
+
const [lastSync] = await ctx.database.get("ggcevo_ban_record", {}, { limit: 1 });
|
|
2190
|
+
if (!lastSync) {
|
|
2191
|
+
return "⚠️ 封禁记录尚未同步, 请稍后再试或联系管理员。";
|
|
2192
|
+
}
|
|
2193
|
+
const syncTime = lastSync.update_time;
|
|
2194
|
+
const allRecords = await ctx.database.get("ggcevo_ban_record", {}, { sort: { id: "asc" } });
|
|
2195
|
+
const target = normalizeHandle(handle);
|
|
2196
|
+
const records = allRecords.filter((r) => r.handle && normalizeHandle(r.handle) === target);
|
|
2197
|
+
if (records.length === 0) {
|
|
2198
|
+
return [
|
|
2199
|
+
`✅ 句柄 ${handle} 暂无封禁记录。`,
|
|
2200
|
+
`📊 数据最近同步: ${toBeijingTime(syncTime.toISOString())}`
|
|
2201
|
+
].join("\n");
|
|
2202
|
+
}
|
|
2203
|
+
const formatRecord = /* @__PURE__ */ __name((idx) => {
|
|
2204
|
+
const r = records[idx];
|
|
2205
|
+
return [
|
|
2206
|
+
`📋 封禁记录 (${idx + 1}/${records.length})`,
|
|
2207
|
+
`文档行号: ${r.id + 1} 行`,
|
|
2208
|
+
// id 对应文档行号 (跳过表头)
|
|
2209
|
+
`句柄: ${r.handle}`,
|
|
2210
|
+
`封禁等级: ${r.ban_level || "-"}`,
|
|
2211
|
+
`处罚原因: ${r.reason || "-"}`,
|
|
2212
|
+
`处罚次数: ${r.count || "-"}`,
|
|
2213
|
+
`审核员: ${r.auditor || "-"}`,
|
|
2214
|
+
`审核时间: ${r.audit_time || "-"}`,
|
|
2215
|
+
"",
|
|
2216
|
+
'💡 回复 "下一页"/"上一页"/页码数字 翻页, 或 "退出" 结束'
|
|
2217
|
+
].join("\n");
|
|
2218
|
+
}, "formatRecord");
|
|
2219
|
+
let page = 0;
|
|
2220
|
+
await session.send([
|
|
2221
|
+
formatRecord(page),
|
|
2222
|
+
`📊 数据最近同步: ${toBeijingTime(syncTime.toISOString())}`
|
|
2223
|
+
].join("\n"));
|
|
2224
|
+
while (true) {
|
|
2225
|
+
const input = await session.prompt(6e4);
|
|
2226
|
+
if (!input) break;
|
|
2227
|
+
const cmd = input.trim();
|
|
2228
|
+
if (/^(退出|exit|q|end)$/i.test(cmd)) break;
|
|
2229
|
+
if (/^(下一页|下页|next|n)$/i.test(cmd)) {
|
|
2230
|
+
if (page < records.length - 1) {
|
|
2231
|
+
page++;
|
|
2232
|
+
await session.send(formatRecord(page));
|
|
2233
|
+
} else await session.send('已是最后一页。回复 "上一页" 或 "退出"。');
|
|
2234
|
+
} else if (/^(上一页|上页|prev|p|上一个)$/i.test(cmd)) {
|
|
2235
|
+
if (page > 0) {
|
|
2236
|
+
page--;
|
|
2237
|
+
await session.send(formatRecord(page));
|
|
2238
|
+
} else await session.send('已是第一页。回复 "下一页" 或 "退出"。');
|
|
2239
|
+
} else {
|
|
2240
|
+
const n = parseInt(cmd, 10);
|
|
2241
|
+
if (!isNaN(n) && n >= 1 && n <= records.length) {
|
|
2242
|
+
page = n - 1;
|
|
2243
|
+
await session.send(formatRecord(page));
|
|
2244
|
+
} else {
|
|
2245
|
+
await session.send('⚠️ 无效输入, 请回复 "下一页"/"上一页"/页码/退出。');
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
return "已退出封禁记录查询。";
|
|
2250
|
+
});
|
|
1920
2251
|
}
|
|
1921
2252
|
__name(apply, "apply");
|
|
1922
2253
|
function toBeijingTime(isoString) {
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -19,6 +19,8 @@ npm install koishi-plugin-ggcevo
|
|
|
19
19
|
|
|
20
20
|
## 配置项
|
|
21
21
|
|
|
22
|
+
### 地图检测
|
|
23
|
+
|
|
22
24
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
23
25
|
| --- | --- | --- | --- |
|
|
24
26
|
| `mapMonitorEnabled` | `boolean` | `false` | 是否启用地图检测定时任务 |
|
|
@@ -26,6 +28,24 @@ npm install koishi-plugin-ggcevo
|
|
|
26
28
|
| `mapMonitorMapIds` | `number[]` | `[]` | 需要检测的地图 ID 列表 |
|
|
27
29
|
| `mapMonitorApiUrl` | `string` | `https://server.dreamprotocol.info:13085/mapmonitor/maps` | 地图检测 API 地址 |
|
|
28
30
|
|
|
31
|
+
### 腾讯文档(封禁记录查询)
|
|
32
|
+
|
|
33
|
+
支持两种授权模式,二选一即可:
|
|
34
|
+
|
|
35
|
+
| 配置项 | 类型 | 默认值 | 说明 |
|
|
36
|
+
| --- | --- | --- | --- |
|
|
37
|
+
| `tencentDocsEnabled` | `boolean` | `false` | 是否启用腾讯文档功能 |
|
|
38
|
+
| `tencentDocsClientId` | `string` | `''` | 应用 Client ID(应用ID) |
|
|
39
|
+
| `tencentDocsClientSecret` | `string` | `''` | 应用 Client Secret(**应用级模式必填**,用户级模式留空) |
|
|
40
|
+
| `tencentDocsAccessToken` | `string` | `''` | Access Token(**用户级模式必填**,通过扫码授权获取) |
|
|
41
|
+
| `tencentDocsOpenId` | `string` | `''` | Open ID(**用户级模式必填**,与 Access Token 同时获取) |
|
|
42
|
+
| `tencentDocsBanListFileId` | `string` | `DTVdYZVBDdFhEUkp6` | 封禁记录在线表格文件ID(短ID或完整ID) |
|
|
43
|
+
| `tencentDocsBanListSheetId` | `string` | `BB08J2` | 封禁记录工作表ID(表格URL中 `tab` 参数) |
|
|
44
|
+
|
|
45
|
+
**授权模式说明:**
|
|
46
|
+
- **用户级模式**(推荐用于无 `Client Secret` 的场景):填写 `tencentDocsClientId` + `tencentDocsAccessToken` + `tencentDocsOpenId`,三者均通过腾讯文档开放平台扫码授权流程获取。Token 过期后需重新获取并在控制台更新。
|
|
47
|
+
- **应用级模式**:填写 `tencentDocsClientId` + `tencentDocsClientSecret`,插件自动获取并刷新 Token,无需手动维护。需应用已开通 `scope.auth.account` 权限。
|
|
48
|
+
|
|
29
49
|
## 指令列表
|
|
30
50
|
|
|
31
51
|
### 星际争霸2 句柄(sc2arcade)
|
|
@@ -57,6 +77,14 @@ npm install koishi-plugin-ggcevo
|
|
|
57
77
|
| `活动列表` | — | — | 查看当前所有活动 |
|
|
58
78
|
| `补签` | — | — | 使用补签券补签漏签的日期 |
|
|
59
79
|
| `使用 <name>` | — | — | 使用指定物品(如赎罪券等) |
|
|
80
|
+
| `封禁记录` | — | — | 查询当前绑定句柄在腾讯文档封禁记录表中的全部记录,每页显示 1 条,支持翻页(回复"下一页/上一页/页码/退出") |
|
|
81
|
+
| `同步封禁记录` | — | 3 | 立即从腾讯文档拉取全量数据并写入数据库(管理员指令) |
|
|
82
|
+
|
|
83
|
+
### 腾讯文档
|
|
84
|
+
|
|
85
|
+
| 指令 | 说明 |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| `腾讯文档/状态` | 查看腾讯文档授权状态(授权模式、Open ID、令牌过期时间等) |
|
|
60
88
|
|
|
61
89
|
## 物品与奖池
|
|
62
90
|
|
|
@@ -93,9 +121,43 @@ npm install koishi-plugin-ggcevo
|
|
|
93
121
|
- `ggcevo_exchange_log` — 兑换记录
|
|
94
122
|
- `ggcevo_activity` — 活动配置
|
|
95
123
|
- `ggcevo_activity_claim_log` — 活动领取记录(主键 `activity_id` + `user_id`)
|
|
124
|
+
- `ggcevo_docs_token` — 腾讯文档令牌缓存(应用级模式使用,主键 `user_id`,应用级账号固定为 `app_account`)
|
|
125
|
+
- `ggcevo_ban_record` — 封禁记录(主键 `id` 自增,对应文档行号 `id+1`,每小时全量同步)
|
|
96
126
|
|
|
97
127
|
## 更新日志
|
|
98
128
|
|
|
129
|
+
### v1.0.6
|
|
130
|
+
|
|
131
|
+
修复封禁记录同步时长文本写入失败的问题。
|
|
132
|
+
|
|
133
|
+
- **修复**:`ggcevo_ban_record` 表 `reason`(处罚原因)字段类型由 `string`(VARCHAR(255))改为 `text`(TEXT,最大 65535 字符),避免处罚原因过长时触发 `ER_DATA_TOO_LONG` 错误
|
|
134
|
+
- 类型变更由 minato 自动执行 `ALTER TABLE`,已有数据不受影响,直接重启即可生效
|
|
135
|
+
|
|
136
|
+
### v1.0.5
|
|
137
|
+
|
|
138
|
+
新增**腾讯文档封禁记录查询**功能,基于腾讯文档开放平台 V3 在线表格接口实现。
|
|
139
|
+
|
|
140
|
+
**新增功能:**
|
|
141
|
+
- **腾讯文档授权**:支持「用户级 Token」与「应用级账号」双授权模式自动识别
|
|
142
|
+
- 用户级模式:填写 `Client ID` + `Access Token` + `Open ID`(适用于无 Client Secret 的场景)
|
|
143
|
+
- 应用级模式:填写 `Client ID` + `Client Secret`(自动获取并刷新 Token,需 `scope.auth.account` 权限)
|
|
144
|
+
- **封禁记录同步**:每小时自动从腾讯文档在线表格全量拉取数据写入数据库(启动后延迟 5 秒首次同步),采用全量替换策略保证 `id` 与文档行号严格对应
|
|
145
|
+
- **封禁记录查询**:`封禁记录` 指令查询当前绑定句柄的全部封禁记录,每页 1 条支持翻页(下一页/上一页/页码/退出),显示文档行号便于定位原文档
|
|
146
|
+
- **手动同步**:`同步封禁记录` 指令(管理员权限 3)立即触发同步
|
|
147
|
+
- **授权状态查询**:`腾讯文档/状态` 指令查看当前授权模式与令牌信息
|
|
148
|
+
|
|
149
|
+
**新增配置项:**
|
|
150
|
+
- `tencentDocsEnabled`、`tencentDocsClientId`、`tencentDocsClientSecret`
|
|
151
|
+
- `tencentDocsAccessToken`、`tencentDocsOpenId`
|
|
152
|
+
- `tencentDocsBanListFileId`(默认 `DTVdYZVBDdFhEUkp6`)、`tencentDocsBanListSheetId`(默认 `BB08J2`)
|
|
153
|
+
|
|
154
|
+
**新增数据库表:**
|
|
155
|
+
- `ggcevo_docs_token` — 令牌缓存(应用级模式)
|
|
156
|
+
- `ggcevo_ban_record` — 封禁记录(主键自增 `id`,对应文档行号 `id+1`)
|
|
157
|
+
|
|
158
|
+
**表格结构要求:**
|
|
159
|
+
- A列:句柄 / C列:封禁等级 / D列:处罚原因 / E列:处罚次数 / F列:审核员 / G列:审核时间
|
|
160
|
+
|
|
99
161
|
### v1.0.4
|
|
100
162
|
|
|
101
163
|
- 新增配置项 `mapMonitorApiUrl`,可将地图检测的 API 地址通过配置项填写,默认值为 `https://server.dreamprotocol.info:13085/mapmonitor/maps`。
|