koishi-plugin-mgtown-update 1.0.0
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 +12 -0
- package/lib/index.js +251 -0
- package/package.json +30 -0
- package/readme.md +97 -0
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Context, Schema } from 'koishi';
|
|
2
|
+
export declare const name = "mgtown-update";
|
|
3
|
+
export interface Config {
|
|
4
|
+
/** API 基础地址,例如 http://192.168.1.100:8080,末尾不要带斜杠 */
|
|
5
|
+
apiUrl: string;
|
|
6
|
+
/** 管理令牌,用于 Authorization 头 */
|
|
7
|
+
adminToken: string;
|
|
8
|
+
/** 获取信息时是否需要令牌(默认 false) */
|
|
9
|
+
getRequiresToken?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare const Config: Schema<Config>;
|
|
12
|
+
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name2 in all)
|
|
8
|
+
__defProp(target, name2, { get: all[name2], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
Config: () => Config,
|
|
24
|
+
apply: () => apply,
|
|
25
|
+
name: () => name
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(src_exports);
|
|
28
|
+
var import_koishi = require("koishi");
|
|
29
|
+
var name = "mgtown-update";
|
|
30
|
+
var Config = import_koishi.Schema.object({
|
|
31
|
+
apiUrl: import_koishi.Schema.string().description("API 的基础 URL,例如 http://192.168.1.100:8080(末尾不要加 /)").required(),
|
|
32
|
+
adminToken: import_koishi.Schema.string().description("管理令牌 (Bearer Token)").required(),
|
|
33
|
+
getRequiresToken: import_koishi.Schema.boolean().description("GET /api/info 请求是否需要携带令牌").default(false)
|
|
34
|
+
});
|
|
35
|
+
function apply(ctx, config) {
|
|
36
|
+
const logger = ctx.logger("mgtown-update");
|
|
37
|
+
async function sendLongText(session, text, maxLength = 2e3) {
|
|
38
|
+
for (let i = 0; i < text.length; i += maxLength) {
|
|
39
|
+
await session.send(text.substring(i, i + maxLength));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
__name(sendLongText, "sendLongText");
|
|
43
|
+
function buildTreeLines(obj, prefix = "", isLast = true) {
|
|
44
|
+
const lines = [];
|
|
45
|
+
const keys = Object.keys(obj);
|
|
46
|
+
for (let i = 0; i < keys.length; i++) {
|
|
47
|
+
const key = keys[i];
|
|
48
|
+
const value = obj[key];
|
|
49
|
+
const isLastItem = i === keys.length - 1;
|
|
50
|
+
const connector = isLastItem ? "└─ " : "├─ ";
|
|
51
|
+
const newPrefix = prefix + (isLast ? " " : "│ ");
|
|
52
|
+
let valueDesc = "";
|
|
53
|
+
if (value === null) {
|
|
54
|
+
valueDesc = "null";
|
|
55
|
+
} else if (Array.isArray(value)) {
|
|
56
|
+
valueDesc = `array[${value.length}]`;
|
|
57
|
+
} else if (typeof value === "object") {
|
|
58
|
+
valueDesc = "object";
|
|
59
|
+
} else if (typeof value === "string") {
|
|
60
|
+
const short = value.length > 30 ? value.substring(0, 30) + "…" : value;
|
|
61
|
+
valueDesc = `"${short}" (string, 长度 ${value.length})`;
|
|
62
|
+
} else {
|
|
63
|
+
valueDesc = String(value) + ` (${typeof value})`;
|
|
64
|
+
}
|
|
65
|
+
lines.push(prefix + connector + key + (valueDesc ? ": " + valueDesc : ""));
|
|
66
|
+
if (value && typeof value === "object") {
|
|
67
|
+
if (Array.isArray(value)) {
|
|
68
|
+
for (let j = 0; j < value.length; j++) {
|
|
69
|
+
const elem = value[j];
|
|
70
|
+
const elemIsLast = j === value.length - 1;
|
|
71
|
+
const elemConnector = elemIsLast ? "└─ " : "├─ ";
|
|
72
|
+
lines.push(newPrefix + (isLastItem ? " " : "│ ") + elemConnector + `[${j}]`);
|
|
73
|
+
if (elem && typeof elem === "object") {
|
|
74
|
+
const subLines = buildTreeLines(elem, newPrefix + (isLastItem ? " " : "│ ") + (elemIsLast ? " " : "│ "), elemIsLast);
|
|
75
|
+
lines.push(...subLines);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
const subLines = buildTreeLines(value, newPrefix, isLastItem);
|
|
80
|
+
lines.push(...subLines);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return lines;
|
|
85
|
+
}
|
|
86
|
+
__name(buildTreeLines, "buildTreeLines");
|
|
87
|
+
ctx.command("town", "管理小镇 API 信息", { authority: 3 });
|
|
88
|
+
ctx.command("town.get", "获取当前小镇信息", { authority: 3 }).action(async ({ session }) => {
|
|
89
|
+
await session.send("正在获取小镇信息,请稍候……");
|
|
90
|
+
const url = `${config.apiUrl}/api/info`;
|
|
91
|
+
const headers = { "Content-Type": "application/json" };
|
|
92
|
+
if (config.getRequiresToken) {
|
|
93
|
+
headers.Authorization = `Bearer ${config.adminToken}`;
|
|
94
|
+
}
|
|
95
|
+
logger.info("正在请求小镇信息");
|
|
96
|
+
logger.info("请求 URL: %s", url);
|
|
97
|
+
logger.info("请求 Headers: %o", headers);
|
|
98
|
+
try {
|
|
99
|
+
const response = await ctx.http.get(url, { headers });
|
|
100
|
+
logger.info("成功获取小镇信息");
|
|
101
|
+
const { success, data, message, timestamp } = response;
|
|
102
|
+
if (!success) {
|
|
103
|
+
return `获取失败:${message || "未知错误"}`;
|
|
104
|
+
}
|
|
105
|
+
const { basic, downloads, news, media } = data;
|
|
106
|
+
if (basic.description) {
|
|
107
|
+
const safeName = (basic.name || "小镇").replace(/[\\/:*?"<>|]/g, "_");
|
|
108
|
+
const filename = `${safeName}_简介.md`;
|
|
109
|
+
try {
|
|
110
|
+
const base64 = Buffer.from(basic.description, "utf-8").toString("base64");
|
|
111
|
+
const dataUrl = `data:text/markdown;charset=utf-8;base64,${base64}`;
|
|
112
|
+
await session.send((0, import_koishi.h)("file", { src: dataUrl, title: filename }));
|
|
113
|
+
logger.info("简介文件发送成功");
|
|
114
|
+
} catch (fileError) {
|
|
115
|
+
logger.error("发送简介文件失败: %s", fileError.message);
|
|
116
|
+
await session.send("简介文件发送失败,现以文本形式发送完整简介:");
|
|
117
|
+
await sendLongText(session, basic.description);
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
await session.send("该小镇暂无简介");
|
|
121
|
+
}
|
|
122
|
+
let info = (0, import_koishi.h)("img", { src: basic.logo }) + `
|
|
123
|
+
🏡 小镇基本信息
|
|
124
|
+
名称:${basic.name}
|
|
125
|
+
地图下载:${downloads.map}
|
|
126
|
+
`;
|
|
127
|
+
await session.send(info);
|
|
128
|
+
if (news.activities?.length) {
|
|
129
|
+
await session.send(`📅 活动列表(共 ${news.activities.length} 条):`);
|
|
130
|
+
for (const act of news.activities) {
|
|
131
|
+
const date = new Date(act.date * 1e3).toLocaleDateString("zh-CN");
|
|
132
|
+
await session.send(`日期:${date}
|
|
133
|
+
内容:${act.content}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (news.updates?.length) {
|
|
137
|
+
await session.send(`🆕 更新列表(共 ${news.updates.length} 条):`);
|
|
138
|
+
for (const up of news.updates) {
|
|
139
|
+
const date = new Date(up.date * 1e3).toLocaleDateString("zh-CN");
|
|
140
|
+
await session.send(`日期:${date}
|
|
141
|
+
内容:${up.content}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (media) {
|
|
145
|
+
let mediaText = `🎬 媒体视频
|
|
146
|
+
`;
|
|
147
|
+
if (media.birthday?.length) mediaText += `生日视频:${media.birthday.join("\n")}
|
|
148
|
+
`;
|
|
149
|
+
if (media.newYear?.length) mediaText += `新年视频:${media.newYear.join("\n")}
|
|
150
|
+
`;
|
|
151
|
+
if (media.related?.length) mediaText += `相关视频:${media.related.join("\n")}
|
|
152
|
+
`;
|
|
153
|
+
await session.send(mediaText);
|
|
154
|
+
}
|
|
155
|
+
await session.send(`⏱️ 信息更新时间:${new Date(timestamp * 1e3).toLocaleString("zh-CN")}`);
|
|
156
|
+
} catch (e) {
|
|
157
|
+
logger.error("获取小镇信息失败: %s", e.message);
|
|
158
|
+
await session.send(`获取失败:${e.message}`);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
ctx.command("town.tree", "以树形图展示小镇信息数据结构", { authority: 3 }).action(async ({ session }) => {
|
|
162
|
+
await session.send("正在生成数据结构树,请稍候……");
|
|
163
|
+
const url = `${config.apiUrl}/api/info`;
|
|
164
|
+
const headers = { "Content-Type": "application/json" };
|
|
165
|
+
if (config.getRequiresToken) {
|
|
166
|
+
headers.Authorization = `Bearer ${config.adminToken}`;
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const response = await ctx.http.get(url, { headers });
|
|
170
|
+
const { success, data } = response;
|
|
171
|
+
if (!success) {
|
|
172
|
+
return "获取数据失败,无法生成树。";
|
|
173
|
+
}
|
|
174
|
+
const treeLines = buildTreeLines(data);
|
|
175
|
+
const treeText = "🏷️ 小镇信息结构树:\n" + treeLines.join("\n");
|
|
176
|
+
await sendLongText(session, treeText);
|
|
177
|
+
} catch (e) {
|
|
178
|
+
logger.error("生成树失败: %s", e.message);
|
|
179
|
+
await session.send(`生成失败:${e.message}`);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
ctx.command("town.update <key> <value:text>", "更新指定字段(支持点分隔路径)", { authority: 3 }).action(async ({ session }, key, value) => {
|
|
183
|
+
if (!key || !value) {
|
|
184
|
+
return "请指定要更新的键和值。";
|
|
185
|
+
}
|
|
186
|
+
await session.send("正在更新小镇信息,请稍候……");
|
|
187
|
+
let parsedValue;
|
|
188
|
+
const trimmed = value.trim();
|
|
189
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
190
|
+
try {
|
|
191
|
+
parsedValue = JSON.parse(trimmed);
|
|
192
|
+
} catch {
|
|
193
|
+
parsedValue = value;
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
parsedValue = value;
|
|
197
|
+
}
|
|
198
|
+
const keys = key.split(".");
|
|
199
|
+
const payload = {};
|
|
200
|
+
let current = payload;
|
|
201
|
+
for (let i = 0; i < keys.length; i++) {
|
|
202
|
+
const k = keys[i];
|
|
203
|
+
if (i === keys.length - 1) {
|
|
204
|
+
current[k] = parsedValue;
|
|
205
|
+
} else {
|
|
206
|
+
current[k] = {};
|
|
207
|
+
current = current[k];
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const url = `${config.apiUrl}/api/admin/info`;
|
|
211
|
+
const headers = {
|
|
212
|
+
"Content-Type": "application/json",
|
|
213
|
+
"Authorization": `Bearer ${config.adminToken}`
|
|
214
|
+
};
|
|
215
|
+
logger.info("正在更新小镇信息,键路径: %s", key);
|
|
216
|
+
logger.debug("请求 URL: %s", url);
|
|
217
|
+
logger.debug("请求 Payload: %o", payload);
|
|
218
|
+
try {
|
|
219
|
+
const res = await ctx.http.post(url, payload, { headers });
|
|
220
|
+
logger.info("更新小镇信息成功");
|
|
221
|
+
let prettyMsg = "";
|
|
222
|
+
if (res.success) {
|
|
223
|
+
prettyMsg += "✅ 更新成功!\n";
|
|
224
|
+
if (res.message) prettyMsg += `📢 消息:${res.message}
|
|
225
|
+
`;
|
|
226
|
+
if (res.data !== void 0 && res.data !== null) {
|
|
227
|
+
const dataStr = typeof res.data === "object" ? JSON.stringify(res.data, null, 2) : String(res.data);
|
|
228
|
+
prettyMsg += `📦 数据:${dataStr}
|
|
229
|
+
`;
|
|
230
|
+
}
|
|
231
|
+
if (res.timestamp) {
|
|
232
|
+
const date = new Date(res.timestamp * 1e3).toLocaleString("zh-CN");
|
|
233
|
+
prettyMsg += `⏱️ 时间:${date}`;
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
prettyMsg += `❌ 更新失败:${res.message || "未知错误"}`;
|
|
237
|
+
}
|
|
238
|
+
await session.send(prettyMsg);
|
|
239
|
+
} catch (e) {
|
|
240
|
+
logger.error("更新小镇信息失败: %s", e.message);
|
|
241
|
+
await session.send(`更新失败:${e.message}`);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
__name(apply, "apply");
|
|
246
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
247
|
+
0 && (module.exports = {
|
|
248
|
+
Config,
|
|
249
|
+
apply,
|
|
250
|
+
name
|
|
251
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "koishi-plugin-mgtown-update",
|
|
3
|
+
"description": "",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"typings": "lib/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib",
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/MGTown/koishi-plugin-mgtown-update.git"
|
|
15
|
+
},
|
|
16
|
+
"koishi": {
|
|
17
|
+
"description": {
|
|
18
|
+
"zh": "毛怪小镇MC服务器数据更新插件"
|
|
19
|
+
},
|
|
20
|
+
"hidden": true
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"chatbot",
|
|
24
|
+
"koishi",
|
|
25
|
+
"plugin"
|
|
26
|
+
],
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"koishi": "^4.18.7"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# koishi-plugin-mgtown-update
|
|
2
|
+
|
|
3
|
+
用于管理小镇 API 信息的 Koishi 插件,支持获取、更新和查看数据结构。基于 [TownAPI 远程更新指南](UPDATE_API.md) 实现。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
在 Koishi 项目中安装本插件:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
yarn add koishi-plugin-mgtown-update
|
|
13
|
+
# 或
|
|
14
|
+
npm install koishi-plugin-mgtown-update
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
然后将插件添加到配置文件(`koishi.config.ts` 或 `koishi.yml`):
|
|
18
|
+
|
|
19
|
+
```yaml
|
|
20
|
+
plugins:
|
|
21
|
+
mgtown-update:
|
|
22
|
+
apiUrl: 'http://你的IP:8080'
|
|
23
|
+
adminToken: '你的管理令牌'
|
|
24
|
+
getRequiresToken: false # 可选,默认为 false
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 配置项
|
|
30
|
+
|
|
31
|
+
### apiUrl
|
|
32
|
+
- **类型**:`string`
|
|
33
|
+
- **必填**:是
|
|
34
|
+
- **描述**:TownAPI 的基础地址,例如 `http://192.168.1.100:8080`,末尾不要带斜杠。
|
|
35
|
+
|
|
36
|
+
### adminToken
|
|
37
|
+
- **类型**:`string`
|
|
38
|
+
- **必填**:是
|
|
39
|
+
- **描述**:用于 API 认证的管理员令牌,对应 `Authorization: Bearer <token>`。
|
|
40
|
+
|
|
41
|
+
### getRequiresToken
|
|
42
|
+
- **类型**:`boolean`
|
|
43
|
+
- **默认值**:`false`
|
|
44
|
+
- **描述**:获取信息(`GET /api/info`)时是否需要携带令牌。如果 API 要求认证,请设置为 `true`。
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 指令说明
|
|
49
|
+
|
|
50
|
+
所有指令需要用户权限等级 **≥ 3**(管理员)。
|
|
51
|
+
|
|
52
|
+
### town.get
|
|
53
|
+
获取当前小镇信息,并分块显示:
|
|
54
|
+
- 将 `basic.description` 作为 Markdown 文件发送。
|
|
55
|
+
- 显示 Logo 图片(`<img>` 元素)。
|
|
56
|
+
- 显示基本信息、活动列表、更新列表、媒体视频和时间戳。
|
|
57
|
+
|
|
58
|
+
### town.tree
|
|
59
|
+
以树形图展示小镇信息的完整数据结构,方便查看所有字段路径。树形图会分段发送以避免消息过长。
|
|
60
|
+
|
|
61
|
+
### town.update <key> <value:text>
|
|
62
|
+
局部更新小镇信息。
|
|
63
|
+
|
|
64
|
+
- **`<key>`**:点分隔的字段路径,例如 `basic.name`、`news.activities`。
|
|
65
|
+
- **`<value>`**:要设置的新值。如果值是 JSON 对象/数组,请提供合法的 JSON 字符串(如 `'{"key":"value"}'`),否则作为普通字符串处理。
|
|
66
|
+
|
|
67
|
+
更新成功后会返回美化后的响应信息。
|
|
68
|
+
|
|
69
|
+
#### 示例
|
|
70
|
+
```bash
|
|
71
|
+
# 修改小镇名称
|
|
72
|
+
town.update basic.name 新小镇名称
|
|
73
|
+
|
|
74
|
+
# 修改 Logo URL
|
|
75
|
+
town.update basic.logo https://example.com/logo.png
|
|
76
|
+
|
|
77
|
+
# 替换活动列表(需提供 JSON 数组)
|
|
78
|
+
town.update news.activities '[{"date": 1771113600, "content": "今天天气不错"}]'
|
|
79
|
+
|
|
80
|
+
# 更新简介(多行文本)
|
|
81
|
+
town.update basic.description "这是新的简介内容。"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 注意事项
|
|
87
|
+
|
|
88
|
+
- 插件依赖 **koishi 4.18.7** 或更高版本。
|
|
89
|
+
- 文件发送功能要求聊天平台支持普通文件传输(如 QQ、Discord 等)。
|
|
90
|
+
- 树形图显示时,字符串超过 30 字符会被截断并显示长度。
|
|
91
|
+
- 如果 `town.get` 发送文件失败,会自动降级为发送文本简介。
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 协议
|
|
96
|
+
|
|
97
|
+
MIT
|