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 CHANGED
@@ -126,7 +126,7 @@ class RocomClient {
126
126
  stack: err?.stack,
127
127
  } : undefined,
128
128
  };
129
- console.warn(`[rocom-client] ${method} ${path} detailed failure\n${this.stringifyForLog(details)}`);
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 {
@@ -161,9 +161,26 @@ function register(deps) {
161
161
  .action(async ({ session }, index) => {
162
162
  if (!index)
163
163
  return '用法:洛克.切换 <序号>';
164
- return userMgr.switchPrimary(session.userId, index)
165
- ? `成功切换到序号 ${index} 账号。`
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('.刷新', '刷新当前主账号凭证')
@@ -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 roleRes = await client.getRole(ctx, fwToken, undefined, userId);
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
- return totalInvalid > 0
87
- ? `清理完成:移除 ${totalInvalid} 个无效绑定,剩余 ${totalValid} 个有效绑定。`
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
- }, 60000);
139
+ }, 30000);
118
140
  }
119
141
  }
@@ -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
- return `${formatTime(start)}-${formatTime(end)}`;
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 items = parts.length ? parts : defaultItems;
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: parts.length ? TEXT.customSource : TEXT.defaultSource,
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 = getActiveProducts(res);
158
+ const { products, roundInfo, data, fallback } = buildMerchantRenderPayload(res);
123
159
  const productNames = products.map((p) => p.name || '').filter(Boolean);
124
- const roundInfo = getCurrentMerchantRound();
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 matched = sub.items.filter((item) => productNames.some(n => n.includes(item)));
130
- if (!matched.length)
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
- matchedCount++;
133
- if (sub.last_push_round === roundInfo.round_id && sameStringArray(matched, sub.last_matched_items || []))
172
+ if (matchAll && !products.length)
134
173
  continue;
135
- const msg = `\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')}`;
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 sent = await (0, subscription_send_1.sendScheduledMessage)(ctx, {
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 ? `@\u5168\u4f53\n${msg}` : msg);
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 products = getActiveProducts(res);
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
- return `${scopeName}订阅商品:${sub.items.join('、')}\n提醒方式:${target.privateChat ? '私聊提醒' : (sub.mention_all ? '@全体' : '普通提醒')}`;
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 }) => {