koishi-plugin-rocom 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/client.js +1 -1
- package/lib/commands/account.js +41 -3
- package/lib/commands/admin.js +27 -5
- package/lib/commands/merchant.js +73 -38
- package/lib/index.js +145 -3815
- package/lib/render-templates/yuanxing-shangren/index.html +29 -29
- package/lib/subscription-send.d.ts +2 -1
- package/lib/subscription-send.js +13 -0
- package/lib/user.d.ts +1 -0
- package/package.json +2 -2
- package/readme.md +22 -10
package/lib/client.js
CHANGED
|
@@ -126,7 +126,7 @@ class RocomClient {
|
|
|
126
126
|
stack: err?.stack,
|
|
127
127
|
} : undefined,
|
|
128
128
|
};
|
|
129
|
-
|
|
129
|
+
logger.warn(`${method} ${path} detailed failure\n${this.stringifyForLog(details)}`);
|
|
130
130
|
}
|
|
131
131
|
async get(ctx, path, headers, params, options) {
|
|
132
132
|
try {
|
package/lib/commands/account.js
CHANGED
|
@@ -161,9 +161,26 @@ function register(deps) {
|
|
|
161
161
|
.action(async ({ session }, index) => {
|
|
162
162
|
if (!index)
|
|
163
163
|
return '用法:洛克.切换 <序号>';
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
164
|
+
const userId = session.userId;
|
|
165
|
+
if (!userMgr.switchPrimary(userId, index))
|
|
166
|
+
return '序号无效。';
|
|
167
|
+
const newPrimary = userMgr.getPrimaryBinding(userId);
|
|
168
|
+
if (newPrimary?.binding_id) {
|
|
169
|
+
const token = await (0, role_token_1.getRoleToken)(ctx, userId);
|
|
170
|
+
if (token && token.bindingId !== newPrimary.binding_id) {
|
|
171
|
+
const res = await client.refreshBinding(ctx, newPrimary.binding_id, userId);
|
|
172
|
+
if (res?.framework_token) {
|
|
173
|
+
await (0, role_token_1.upsertRoleToken)(ctx, {
|
|
174
|
+
userId,
|
|
175
|
+
fwt: res.framework_token,
|
|
176
|
+
bindingId: newPrimary.binding_id,
|
|
177
|
+
roleId: newPrimary.role_id,
|
|
178
|
+
loginType: newPrimary.login_type,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return `成功切换到序号 ${index} 账号:${newPrimary?.nickname || '未知'}`;
|
|
167
184
|
});
|
|
168
185
|
ctx.command('洛克').subcommand('.解绑 <index:number>', '解绑账号')
|
|
169
186
|
.alias('洛克解绑')
|
|
@@ -184,6 +201,27 @@ function register(deps) {
|
|
|
184
201
|
if (userMgr.getUserBindings(session.userId).length === 0) {
|
|
185
202
|
await (0, role_token_1.removeRoleToken)(ctx, session.userId);
|
|
186
203
|
}
|
|
204
|
+
else {
|
|
205
|
+
const token = await (0, role_token_1.getRoleToken)(ctx, session.userId);
|
|
206
|
+
if (token?.bindingId === removed.binding_id) {
|
|
207
|
+
const newPrimary = userMgr.getPrimaryBinding(session.userId);
|
|
208
|
+
if (newPrimary?.binding_id) {
|
|
209
|
+
const res = await client.refreshBinding(ctx, newPrimary.binding_id, session.userId);
|
|
210
|
+
if (res?.framework_token) {
|
|
211
|
+
await (0, role_token_1.upsertRoleToken)(ctx, {
|
|
212
|
+
userId: session.userId,
|
|
213
|
+
fwt: res.framework_token,
|
|
214
|
+
bindingId: newPrimary.binding_id,
|
|
215
|
+
roleId: newPrimary.role_id,
|
|
216
|
+
loginType: newPrimary.login_type,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
await (0, role_token_1.removeRoleToken)(ctx, session.userId);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
187
225
|
return `已解绑账号:${removed.nickname}`;
|
|
188
226
|
});
|
|
189
227
|
ctx.command('洛克').subcommand('.刷新', '刷新当前主账号凭证')
|
package/lib/commands/admin.js
CHANGED
|
@@ -53,19 +53,33 @@ function register(deps) {
|
|
|
53
53
|
const allUsers = userMgr.getAllUsersBindings();
|
|
54
54
|
let totalInvalid = 0;
|
|
55
55
|
let totalValid = 0;
|
|
56
|
+
let totalSkipped = 0;
|
|
56
57
|
for (const [userId, bindings] of Object.entries(allUsers)) {
|
|
57
58
|
const token = await (0, role_token_1.getRoleToken)(ctx, userId);
|
|
58
59
|
if (!token?.fwt) {
|
|
59
60
|
logger.warn(`用户 ${userId} 没有可用的 fwt,跳过失效检测`);
|
|
61
|
+
totalSkipped += bindings.length;
|
|
60
62
|
continue;
|
|
61
63
|
}
|
|
62
|
-
const fwToken = token.fwt;
|
|
63
64
|
const valid = [];
|
|
64
65
|
for (const binding of bindings) {
|
|
65
|
-
const
|
|
66
|
+
const refreshRes = binding.binding_id
|
|
67
|
+
? await client.refreshBinding(ctx, binding.binding_id, userId)
|
|
68
|
+
: null;
|
|
69
|
+
const fwt = refreshRes?.framework_token || token.fwt;
|
|
70
|
+
const roleRes = await client.getRole(ctx, fwt, undefined, userId);
|
|
66
71
|
if (roleRes?.role) {
|
|
67
72
|
valid.push(binding);
|
|
68
73
|
totalValid++;
|
|
74
|
+
if (refreshRes?.framework_token) {
|
|
75
|
+
await (0, role_token_1.upsertRoleToken)(ctx, {
|
|
76
|
+
userId,
|
|
77
|
+
fwt: refreshRes.framework_token,
|
|
78
|
+
bindingId: binding.binding_id,
|
|
79
|
+
roleId: binding.role_id,
|
|
80
|
+
loginType: binding.login_type,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
69
83
|
continue;
|
|
70
84
|
}
|
|
71
85
|
if (binding.binding_id) {
|
|
@@ -83,16 +97,24 @@ function register(deps) {
|
|
|
83
97
|
await (0, role_token_1.removeRoleToken)(ctx, userId);
|
|
84
98
|
}
|
|
85
99
|
}
|
|
86
|
-
|
|
87
|
-
|
|
100
|
+
const parts = [`清理完成:移除 ${totalInvalid} 个无效绑定,剩余 ${totalValid} 个有效绑定。`];
|
|
101
|
+
if (totalSkipped)
|
|
102
|
+
parts.push(`跳过 ${totalSkipped} 个(无可用凭证)。`);
|
|
103
|
+
return totalInvalid > 0 || totalSkipped > 0
|
|
104
|
+
? parts.join('')
|
|
88
105
|
: `所有绑定均有效,无需清理。共 ${totalValid} 个有效绑定。`;
|
|
89
106
|
});
|
|
90
107
|
if (config.autoRefreshEnabled) {
|
|
108
|
+
let lastRefreshKey = '';
|
|
91
109
|
ctx.setInterval(async () => {
|
|
92
110
|
const now = new Date();
|
|
93
111
|
const timeStr = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
|
94
112
|
if (!config.autoRefreshTime.includes(timeStr))
|
|
95
113
|
return;
|
|
114
|
+
const refreshKey = `${now.toDateString()}-${timeStr}`;
|
|
115
|
+
if (refreshKey === lastRefreshKey)
|
|
116
|
+
return;
|
|
117
|
+
lastRefreshKey = refreshKey;
|
|
96
118
|
const allUsers = userMgr.getAllUsersBindings();
|
|
97
119
|
for (const [userId] of Object.entries(allUsers)) {
|
|
98
120
|
const binding = userMgr.getPrimaryBinding(userId);
|
|
@@ -114,6 +136,6 @@ function register(deps) {
|
|
|
114
136
|
logger.warn(`自动刷新用户 ${userId} 失败: ${e}`);
|
|
115
137
|
}
|
|
116
138
|
}
|
|
117
|
-
},
|
|
139
|
+
}, 30000);
|
|
118
140
|
}
|
|
119
141
|
}
|
package/lib/commands/merchant.js
CHANGED
|
@@ -28,17 +28,25 @@ function formatProductWindow(product) {
|
|
|
28
28
|
const end = normalizeTimestamp(product?.end_time);
|
|
29
29
|
if (!start && !end)
|
|
30
30
|
return '';
|
|
31
|
+
const formatDate = (timestamp) => {
|
|
32
|
+
const date = new Date(timestamp);
|
|
33
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
34
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
35
|
+
return `${month}-${day}`;
|
|
36
|
+
};
|
|
31
37
|
const formatTime = (timestamp) => {
|
|
32
38
|
const date = new Date(timestamp);
|
|
33
39
|
const hour = String(date.getHours()).padStart(2, '0');
|
|
34
40
|
const minute = String(date.getMinutes()).padStart(2, '0');
|
|
35
41
|
return `${hour}:${minute}`;
|
|
36
42
|
};
|
|
37
|
-
if (start && end)
|
|
38
|
-
|
|
43
|
+
if (start && end) {
|
|
44
|
+
const datePart = formatDate(start);
|
|
45
|
+
return `${datePart} ${formatTime(start)} – ${formatTime(end)}`;
|
|
46
|
+
}
|
|
39
47
|
if (start)
|
|
40
|
-
return `${formatTime(start)}+`;
|
|
41
|
-
return `${formatTime(end)}-`;
|
|
48
|
+
return `${formatDate(start)} ${formatTime(start)}+`;
|
|
49
|
+
return `${formatDate(end)} ${formatTime(end)}-`;
|
|
42
50
|
}
|
|
43
51
|
function getMerchantActivity(res) {
|
|
44
52
|
const activities = res?.merchantActivities || res?.merchant_activities || [];
|
|
@@ -83,6 +91,7 @@ function getCurrentMerchantRound() {
|
|
|
83
91
|
].join('-');
|
|
84
92
|
return {
|
|
85
93
|
current: currentRound,
|
|
94
|
+
total: rounds.length,
|
|
86
95
|
countdown: `${hours}\u5c0f\u65f6${mins}\u5206\u949f`,
|
|
87
96
|
is_open: currentRound !== null,
|
|
88
97
|
round_id: `${datePart}-${currentRound || 'closed'}`,
|
|
@@ -94,11 +103,16 @@ function parseMerchantSubscriptionArgs(args, defaultItems) {
|
|
|
94
103
|
if (parts[0] === '1' || parts[0] === '0') {
|
|
95
104
|
mentionAll = parts.shift() === '1';
|
|
96
105
|
}
|
|
97
|
-
const
|
|
106
|
+
const matchAll = parts.length === 1 && parts[0] === '全部';
|
|
107
|
+
if (matchAll)
|
|
108
|
+
parts.shift();
|
|
109
|
+
const items = matchAll ? [] : (parts.length ? parts : defaultItems);
|
|
110
|
+
const source = matchAll ? '全部商品' : (parts.length ? TEXT.customSource : TEXT.defaultSource);
|
|
98
111
|
return {
|
|
99
112
|
mention_all: mentionAll,
|
|
113
|
+
match_all: matchAll,
|
|
100
114
|
items,
|
|
101
|
-
source
|
|
115
|
+
source,
|
|
102
116
|
};
|
|
103
117
|
}
|
|
104
118
|
function getSubscriptionTarget(session) {
|
|
@@ -114,37 +128,74 @@ function isBotAdmin(session, adminUserIds) {
|
|
|
114
128
|
function sameStringArray(left, right) {
|
|
115
129
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
116
130
|
}
|
|
131
|
+
function buildMerchantRenderPayload(res) {
|
|
132
|
+
const products = getActiveProducts(res);
|
|
133
|
+
const roundInfo = getCurrentMerchantRound();
|
|
134
|
+
const activity = getMerchantActivity(res);
|
|
135
|
+
const data = {
|
|
136
|
+
background: '',
|
|
137
|
+
title: activity.name || TEXT.merchant,
|
|
138
|
+
subtitle: activity.start_date || '\u6bcf\u65e5 08:00 / 12:00 / 16:00 / 20:00 \u5237\u65b0',
|
|
139
|
+
titleIcon: true,
|
|
140
|
+
product_count: products.length,
|
|
141
|
+
round_info: roundInfo,
|
|
142
|
+
products: products.map((p) => ({
|
|
143
|
+
name: p.name || TEXT.unknown,
|
|
144
|
+
image: p.icon_url || '',
|
|
145
|
+
time_label: formatProductWindow(p),
|
|
146
|
+
})),
|
|
147
|
+
};
|
|
148
|
+
const fallback = products.length
|
|
149
|
+
? `\u8fdc\u884c\u5546\u4eba\u5f53\u524d\u5546\u54c1\uff1a${products.map((p) => p.name || TEXT.unknown).join('\u3001')}\n\u8f6e\u6b21\uff1a${roundInfo.current || TEXT.notOpen}\n\u5269\u4f59\uff1a${roundInfo.countdown}`
|
|
150
|
+
: '\u5f53\u524d\u8fdc\u884c\u5546\u4eba\u6682\u65e0\u5546\u54c1\u3002';
|
|
151
|
+
return { products, roundInfo, data, fallback };
|
|
152
|
+
}
|
|
117
153
|
async function checkMerchantSubscriptions(deps) {
|
|
118
|
-
const { ctx, client, merchantSubMgr } = deps;
|
|
154
|
+
const { ctx, client, merchantSubMgr, renderer, config } = deps;
|
|
119
155
|
const res = await client.getMerchantInfo(ctx, true);
|
|
120
156
|
if (!res)
|
|
121
157
|
return { subscriptions: 0, matched: 0, pushed: 0 };
|
|
122
|
-
const products =
|
|
158
|
+
const { products, roundInfo, data, fallback } = buildMerchantRenderPayload(res);
|
|
123
159
|
const productNames = products.map((p) => p.name || '').filter(Boolean);
|
|
124
|
-
const
|
|
160
|
+
const rendered = await renderer.renderHtml(ctx, 'yuanxing-shangren', data);
|
|
161
|
+
const renderedPng = rendered ? (0, send_image_1.compressPngImage)(rendered, config) : null;
|
|
125
162
|
const subs = merchantSubMgr.getAll();
|
|
126
163
|
let matchedCount = 0;
|
|
127
164
|
let pushedCount = 0;
|
|
128
165
|
for (const [key, sub] of Object.entries(subs)) {
|
|
129
|
-
const
|
|
130
|
-
|
|
166
|
+
const matchAll = !!sub.match_all;
|
|
167
|
+
const matched = matchAll
|
|
168
|
+
? productNames
|
|
169
|
+
: sub.items.filter((item) => productNames.some(n => n.includes(item)));
|
|
170
|
+
if (!matchAll && !matched.length)
|
|
131
171
|
continue;
|
|
132
|
-
|
|
133
|
-
if (sub.last_push_round === roundInfo.round_id && sameStringArray(matched, sub.last_matched_items || []))
|
|
172
|
+
if (matchAll && !products.length)
|
|
134
173
|
continue;
|
|
135
|
-
|
|
174
|
+
matchedCount++;
|
|
175
|
+
if (matchAll) {
|
|
176
|
+
if (sub.last_push_round === roundInfo.round_id)
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
if (sub.last_push_round === roundInfo.round_id && sameStringArray(matched, sub.last_matched_items || []))
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const msg = matchAll
|
|
184
|
+
? `\ud83d\udd14 \u8fdc\u884c\u5546\u4eba\u5237\u65b0\u63d0\u9192\n\u5f53\u524d\u5546\u54c1\uff1a${productNames.join('\u3001')}`
|
|
185
|
+
: `\ud83d\udd14 \u8fdc\u884c\u5546\u4eba\u5237\u65b0\u63d0\u9192\n\u5f53\u524d\u5546\u54c1\uff1a${productNames.join('\u3001')}\n\u5339\u914d\u8ba2\u9605\uff1a${matched.join('\u3001')}`;
|
|
136
186
|
const platform = sub.platform || ctx.bots[0]?.platform;
|
|
137
187
|
const channelId = sub.channel_id || sub.group_id || sub.user_id || key;
|
|
138
188
|
if (!platform || !channelId) {
|
|
139
189
|
logger.warn(`\u63a8\u9001\u5931\u8d25 ${key}: \u65e0\u6cd5\u786e\u5b9a\u5e73\u53f0\u6216\u9891\u9053`);
|
|
140
190
|
continue;
|
|
141
191
|
}
|
|
142
|
-
const
|
|
192
|
+
const fallbackText = `${msg}\n${fallback}`;
|
|
193
|
+
const sent = await (0, subscription_send_1.sendScheduledImageWithFallback)(ctx, {
|
|
143
194
|
platform,
|
|
144
195
|
channelId,
|
|
145
196
|
guildId: sub.group_id || '',
|
|
146
197
|
userId: sub.user_id || '',
|
|
147
|
-
}, sub.mention_all
|
|
198
|
+
}, renderedPng, fallbackText, !!sub.mention_all);
|
|
148
199
|
if (!sent)
|
|
149
200
|
continue;
|
|
150
201
|
pushedCount++;
|
|
@@ -163,25 +214,7 @@ function register(deps) {
|
|
|
163
214
|
const res = await client.getMerchantInfo(ctx, true);
|
|
164
215
|
if (!res)
|
|
165
216
|
return '\u83b7\u53d6\u8fdc\u884c\u5546\u4eba\u6570\u636e\u5931\u8d25\u3002';
|
|
166
|
-
const
|
|
167
|
-
const roundInfo = getCurrentMerchantRound();
|
|
168
|
-
const activity = getMerchantActivity(res);
|
|
169
|
-
const data = {
|
|
170
|
-
background: '',
|
|
171
|
-
title: activity.name || TEXT.merchant,
|
|
172
|
-
subtitle: activity.start_date || '\u6bcf\u65e5 08:00 / 12:00 / 16:00 / 20:00 \u5237\u65b0',
|
|
173
|
-
titleIcon: '',
|
|
174
|
-
product_count: products.length,
|
|
175
|
-
round_info: roundInfo,
|
|
176
|
-
products: products.map((p) => ({
|
|
177
|
-
name: p.name || TEXT.unknown,
|
|
178
|
-
image: p.icon_url || '',
|
|
179
|
-
time_label: formatProductWindow(p),
|
|
180
|
-
})),
|
|
181
|
-
};
|
|
182
|
-
const fallback = products.length
|
|
183
|
-
? `\u8fdc\u884c\u5546\u4eba\u5f53\u524d\u5546\u54c1\uff1a${products.map((p) => p.name || TEXT.unknown).join('\u3001')}\n\u8f6e\u6b21\uff1a${roundInfo.current || TEXT.notOpen}\n\u5269\u4f59\uff1a${roundInfo.countdown}`
|
|
184
|
-
: '\u5f53\u524d\u8fdc\u884c\u5546\u4eba\u6682\u65e0\u5546\u54c1\u3002';
|
|
217
|
+
const { data, fallback } = buildMerchantRenderPayload(res);
|
|
185
218
|
const png = await deps.renderer.renderHtml(ctx, 'yuanxing-shangren', data);
|
|
186
219
|
await (0, send_image_1.sendImageWithFallback)(session, png, fallback, 'merchant:yuanxing-shangren', deps.config);
|
|
187
220
|
});
|
|
@@ -201,12 +234,13 @@ function register(deps) {
|
|
|
201
234
|
channel_id: target.channelId,
|
|
202
235
|
platform: target.platform,
|
|
203
236
|
items: parsed.items,
|
|
237
|
+
match_all: parsed.match_all,
|
|
204
238
|
mention_all: target.privateChat ? false : parsed.mention_all,
|
|
205
239
|
last_push_round: existing?.last_push_round ?? null,
|
|
206
240
|
last_matched_items: existing?.last_matched_items ?? [],
|
|
207
241
|
updated_by: session.userId,
|
|
208
242
|
});
|
|
209
|
-
return `\u2705 \u5df2\u8ba2\u9605\u8fdc\u884c\u5546\u4eba\u5546\u54c1\uff1a${parsed.items.join('\u3001')}\uff08${parsed.source}\uff09\uff1b${target.privateChat ? '个人订阅' : (parsed.mention_all ? '\u547d\u4e2d\u540e\u4f1a @\u5168\u4f53' : '\u547d\u4e2d\u540e\u4e0d @\u5168\u4f53')}`;
|
|
243
|
+
return `\u2705 \u5df2\u8ba2\u9605\u8fdc\u884c\u5546\u4eba\u5546\u54c1\uff1a${parsed.match_all ? '\u5168\u90e8\u5546\u54c1\uff08\u6bcf\u8f6e\u63a8\u9001\uff09' : parsed.items.join('\u3001')}\uff08${parsed.source}\uff09\uff1b${target.privateChat ? '个人订阅' : (parsed.mention_all ? '\u547d\u4e2d\u540e\u4f1a @\u5168\u4f53' : '\u547d\u4e2d\u540e\u4e0d @\u5168\u4f53')}`;
|
|
210
244
|
});
|
|
211
245
|
ctx.command(TEXT.viewSubscribe, '\u67e5\u770b\u5f53\u524d\u4f1a\u8bdd\u7684\u8fdc\u884c\u5546\u4eba\u8ba2\u9605')
|
|
212
246
|
.action(async ({ session }) => {
|
|
@@ -216,8 +250,9 @@ function register(deps) {
|
|
|
216
250
|
const sub = merchantSubMgr.get(target.key);
|
|
217
251
|
const scopeName = target.privateChat ? '你' : '当前群组';
|
|
218
252
|
if (!sub)
|
|
219
|
-
return `${scopeName}未订阅远行商人。\n用法:${TEXT.subscribe} [1/0] [商品名1] [商品名2]
|
|
220
|
-
|
|
253
|
+
return `${scopeName}未订阅远行商人。\n用法:${TEXT.subscribe} [1/0] [商品名1] [商品名2] ...\n或:${TEXT.subscribe} 全部(每轮直接推送整张商人图)`;
|
|
254
|
+
const itemsText = sub.match_all ? '全部商品(每轮推送)' : sub.items.join('、');
|
|
255
|
+
return `${scopeName}订阅商品:${itemsText}\n提醒方式:${target.privateChat ? '私聊提醒' : (sub.mention_all ? '@全体' : '普通提醒')}`;
|
|
221
256
|
});
|
|
222
257
|
ctx.command(TEXT.unsubscribe, '\u53d6\u6d88\u8fdc\u884c\u5546\u4eba\u8ba2\u9605')
|
|
223
258
|
.action(async ({ session }) => {
|