@cartanova/qgrid-cli 2.6.6 → 2.6.7
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/bundle/dist/application/qgrid/expired-token-reminder.js +13 -7
- package/bundle/src/application/qgrid/expired-token-reminder.test.ts +31 -11
- package/bundle/src/application/qgrid/expired-token-reminder.ts +20 -11
- package/bundle/web-dist/client/assets/{index-qs8K8bOv.js → index-CV_rp3zE.js} +3 -3
- package/bundle/web-dist/client/assets/{logs-CvmI4KuC.js → logs-CUO9Q0UT.js} +1 -1
- package/bundle/web-dist/client/assets/{monit-CUueNzoL.js → monit-Dy7UtqC2.js} +1 -1
- package/bundle/web-dist/client/assets/{routes-CAfjRsMY.js → routes-B6gfQrph.js} +1 -1
- package/bundle/web-dist/client/assets/{show-C5vKpp5z.js → show-Bi0QYUVm.js} +1 -1
- package/bundle/web-dist/client/assets/{tokens-C9KKP2Yq.js → tokens-BAuYgBl6.js} +1 -1
- package/bundle/web-dist/client/assets/{use-oauth-login-flow-B3o4CXDW.js → use-oauth-login-flow-Cx6BlvXM.js} +1 -1
- package/bundle/web-dist/client/index.html +1 -1
- package/bundle/web-dist/server/entry-server.generated.js +1 -1
- package/package.json +1 -1
- package/skills/qgrid/references/token-auth-quota-lifecycle.md +1 -1
|
@@ -17,12 +17,18 @@ import { getLogger } from "@logtape/logtape";
|
|
|
17
17
|
*/
|
|
18
18
|
const logger = getLogger(["qgrid", "expired-reminder"]);
|
|
19
19
|
const MINUTE_MS = 6e4;
|
|
20
|
+
/**
|
|
21
|
+
* 1건이면 제목(subject)에 토큰명이 이미 있으므로 본문에서는 뺀다 — 같은 이름을 두 번 쓰면
|
|
22
|
+
* 읽는 사람이 다른 정보인 줄 알고 한 번 더 본다. 여러 건이면 제목이 건수라 본문에 남긴다.
|
|
23
|
+
*/
|
|
20
24
|
function buildReminderContext(tokens, userMap) {
|
|
21
|
-
|
|
25
|
+
const showLabel = tokens.length > 1;
|
|
26
|
+
return [...tokens.map((token) => {
|
|
22
27
|
const mention = mentionFor(token.name, userMap);
|
|
23
28
|
const label = token.name ?? "unnamed";
|
|
29
|
+
if (!showLabel) return mention;
|
|
24
30
|
return mention ? `${mention} ${label}` : label;
|
|
25
|
-
}).join("\n");
|
|
31
|
+
}), "재로그인이 필요합니다"].filter(Boolean).join("\n");
|
|
26
32
|
}
|
|
27
33
|
async function sendReminder(userMap) {
|
|
28
34
|
const inactive = await TokenModel.findInactive("A");
|
|
@@ -30,7 +36,7 @@ async function sendReminder(userMap) {
|
|
|
30
36
|
await notifySlack({
|
|
31
37
|
title: "세션 만료",
|
|
32
38
|
subject: inactive.length > 1 ? `${inactive.length}건` : inactive[0].name ?? "unnamed",
|
|
33
|
-
context:
|
|
39
|
+
context: buildReminderContext(inactive, userMap),
|
|
34
40
|
color: SLACK_COLOR.bad
|
|
35
41
|
});
|
|
36
42
|
}
|
|
@@ -46,9 +52,9 @@ function startExpiredTokenReminder() {
|
|
|
46
52
|
if (!Number.isFinite(minutes) || minutes <= 0) return;
|
|
47
53
|
const userMap = getSlackUserMap();
|
|
48
54
|
logger.info(`expired token reminder every ${minutes}m (${userMap.size} user mappings)`);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
const run = () => sendReminder(userMap).catch((e) => logger.warn(`reminder failed: ${e.message}`));
|
|
56
|
+
run();
|
|
57
|
+
timer = setInterval(run, minutes * MINUTE_MS);
|
|
52
58
|
}
|
|
53
59
|
function stopExpiredTokenReminder() {
|
|
54
60
|
if (!timer) return;
|
|
@@ -58,4 +64,4 @@ function stopExpiredTokenReminder() {
|
|
|
58
64
|
//#endregion
|
|
59
65
|
export { buildReminderContext, startExpiredTokenReminder, stopExpiredTokenReminder };
|
|
60
66
|
|
|
61
|
-
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXhwaXJlZC10b2tlbi1yZW1pbmRlci5qcyIsIm5hbWVzIjpbXSwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvYXBwbGljYXRpb24vcWdyaWQvZXhwaXJlZC10b2tlbi1yZW1pbmRlci50cyJdLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIOyEuOyFmCDrp4zro4wg7JWM66a8IOuwmOuztS5cbiAqXG4gKiDrp4zro4wg7Iic6rCE7J2YIOyVjOumvChgdG9rZW4tZGVhdGgudHNgKeydgCDqt7jrlYwg7LGE64SQ7J2EIOuztOyngCDslYrsnLzrqbQg64aT7Lmc64ukLiDsnqzroZzqt7jsnbjsnYAg7IKs656M7J20XG4gKiDtlbTslbwg7ZWY64qUIOydvOydtOudvCDrhpPsuZjrqbQg7Yag7YGw7J20IOqzhOyGjSDruaDsp4Qg7LGE66GcIOuCqOuKlOuLpC4g6re4656Y7IScIOyVhOyngSDsnqzroZzqt7jsnbjrkJjsp4Ag7JWK7J2AXG4gKiDthqDtgbDsnYQg6rCZ7J2AIOusuOq1rOuhnCDri6Tsi5wg7JWM66aw64ukIOKAlCDsgqzqsbTsnbQg7ZWY64KY7J2066+A66GcIOyVjOumvOuPhCDtlZwg7KKF66WY64ukLlxuICpcbiAqIOyXrOufrCDthqDtgbDsnbQg66eM66OM64+
|
|
67
|
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXhwaXJlZC10b2tlbi1yZW1pbmRlci5qcyIsIm5hbWVzIjpbXSwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvYXBwbGljYXRpb24vcWdyaWQvZXhwaXJlZC10b2tlbi1yZW1pbmRlci50cyJdLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIOyEuOyFmCDrp4zro4wg7JWM66a8IOuwmOuztS5cbiAqXG4gKiDrp4zro4wg7Iic6rCE7J2YIOyVjOumvChgdG9rZW4tZGVhdGgudHNgKeydgCDqt7jrlYwg7LGE64SQ7J2EIOuztOyngCDslYrsnLzrqbQg64aT7Lmc64ukLiDsnqzroZzqt7jsnbjsnYAg7IKs656M7J20XG4gKiDtlbTslbwg7ZWY64qUIOydvOydtOudvCDrhpPsuZjrqbQg7Yag7YGw7J20IOqzhOyGjSDruaDsp4Qg7LGE66GcIOuCqOuKlOuLpC4g6re4656Y7IScIOyVhOyngSDsnqzroZzqt7jsnbjrkJjsp4Ag7JWK7J2AXG4gKiDthqDtgbDsnYQg6rCZ7J2AIOusuOq1rOuhnCDri6Tsi5wg7JWM66aw64ukIOKAlCDsgqzqsbTsnbQg7ZWY64KY7J2066+A66GcIOyVjOumvOuPhCDtlZwg7KKF66WY64ukLlxuICpcbiAqIOyXrOufrCDthqDtgbDsnbQg66eM66OM64+8IOyeiOycvOuptCDtlZwg66mU7Iuc7KeA7JeQIOuqqOydgOuLpC4g7Yag7YGw66eI64ukIOuplOyLnOyngOulvCDrs7TrgrTrqbQg7LGE64SQ7J20IOyLnOuBhOufrOybjFxuICog7KCV7J6RIOydve2eiOyngCDslYrripTri6QuXG4gKlxuICog67mE7Zmc7ISxIO2GoO2BsOydtCDsl4bsnLzrqbQg67O064K07KeAIOyViuuKlOuLpCDigJQgXCLsnbTsg4Eg7JeG7J2MXCLsnYQg7KO86riw7KCB7Jy866GcIOyVjOumrOuptCDssYTrhJDsnbQg66y0646M7KeE64ukLlxuICovXG5pbXBvcnQgeyBnZXRMb2dnZXIgfSBmcm9tIFwiQGxvZ3RhcGUvbG9ndGFwZVwiO1xuXG5pbXBvcnQgeyBub3RpZnlTbGFjaywgU0xBQ0tfQ09MT1IgfSBmcm9tIFwiLi4vLi4vdXRpbHMvc2xhY2stbm90aWZ5XCI7XG5pbXBvcnQgeyBUb2tlbk1vZGVsIH0gZnJvbSBcIi4uL3Rva2VuL3Rva2VuLm1vZGVsXCI7XG5pbXBvcnQgeyBnZXRTbGFja1VzZXJNYXAsIG1lbnRpb25Gb3IgfSBmcm9tIFwiLi9zbGFjay11c2VyLW1hcFwiO1xuXG5jb25zdCBsb2dnZXIgPSBnZXRMb2dnZXIoW1wicWdyaWRcIiwgXCJleHBpcmVkLXJlbWluZGVyXCJdKTtcblxuY29uc3QgTUlOVVRFX01TID0gNjBfMDAwO1xuXG4vKipcbiAqIDHqsbTsnbTrqbQg7KCc66qpKHN1YmplY3Qp7JeQIO2GoO2BsOuqheydtCDsnbTrr7gg7J6I7Jy866+A66GcIOuzuOusuOyXkOyEnOuKlCDruoDri6Qg4oCUIOqwmeydgCDsnbTrpoTsnYQg65GQIOuyiCDsk7DrqbRcbiAqIOydveuKlCDsgqzrnozsnbQg64uk66W4IOygleuztOyduCDspIQg7JWM6rOgIO2VnCDrsogg642UIOuzuOuLpC4g7Jes65+sIOqxtOydtOuptCDsoJzrqqnsnbQg6rG07IiY6528IOuzuOusuOyXkCDrgqjquLTri6QuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBidWlsZFJlbWluZGVyQ29udGV4dChcbiAgdG9rZW5zOiB7IG5hbWU6IHN0cmluZyB8IG51bGw7IHByb3ZpZGVyOiBzdHJpbmcgfVtdLFxuICB1c2VyTWFwOiBNYXA8c3RyaW5nLCBzdHJpbmc+LFxuKTogc3RyaW5nIHtcbiAgY29uc3Qgc2hvd0xhYmVsID0gdG9rZW5zLmxlbmd0aCA+IDE7XG4gIGNvbnN0IGxpbmVzID0gdG9rZW5zLm1hcCgodG9rZW4pID0+IHtcbiAgICBjb25zdCBtZW50aW9uID0gbWVudGlvbkZvcih0b2tlbi5uYW1lLCB1c2VyTWFwKTtcbiAgICBjb25zdCBsYWJlbCA9IHRva2VuLm5hbWUgPz8gXCJ1bm5hbWVkXCI7XG4gICAgaWYgKCFzaG93TGFiZWwpIHJldHVybiBtZW50aW9uO1xuICAgIHJldHVybiBtZW50aW9uID8gYCR7bWVudGlvbn0gJHtsYWJlbH1gIDogbGFiZWw7XG4gIH0pO1xuICByZXR1cm4gWy4uLmxpbmVzLCBcIuyerOuhnOq3uOyduOydtCDtlYTsmpTtlanri4jri6RcIl0uZmlsdGVyKEJvb2xlYW4pLmpvaW4oXCJcXG5cIik7XG59XG5cbmFzeW5jIGZ1bmN0aW9uIHNlbmRSZW1pbmRlcih1c2VyTWFwOiBNYXA8c3RyaW5nLCBzdHJpbmc+KTogUHJvbWlzZTx2b2lkPiB7XG4gIGNvbnN0IGluYWN0aXZlID0gYXdhaXQgVG9rZW5Nb2RlbC5maW5kSW5hY3RpdmUoXCJBXCIpO1xuICBpZiAoaW5hY3RpdmUubGVuZ3RoID09PSAwKSByZXR1cm47XG5cbiAgYXdhaXQgbm90aWZ5U2xhY2soe1xuICAgIC8vIOunjOujjCDsiJzqsIQg7JWM66a86rO8IOqwmeydgCDsoJzrqqnsnYQg7JO064ukLiDqsJnsnYAg7IKs6rG07J2066+A66GcIOuLpOuluCDsnbTrpoTsnYQg67aZ7J2066m0IOuzhOqwnCDrrLjsoJzroZwg7J297Z6M64ukLlxuICAgIHRpdGxlOiBcIuyEuOyFmCDrp4zro4xcIixcbiAgICBzdWJqZWN0OiBpbmFjdGl2ZS5sZW5ndGggPiAxID8gYCR7aW5hY3RpdmUubGVuZ3RofeqxtGAgOiAoaW5hY3RpdmVbMF0hLm5hbWUgPz8gXCJ1bm5hbWVkXCIpLFxuICAgIGNvbnRleHQ6IGJ1aWxkUmVtaW5kZXJDb250ZXh0KGluYWN0aXZlLCB1c2VyTWFwKSxcbiAgICBjb2xvcjogU0xBQ0tfQ09MT1IuYmFkLFxuICB9KTtcbn1cblxubGV0IHRpbWVyOiBSZXR1cm5UeXBlPHR5cGVvZiBzZXRJbnRlcnZhbD4gfCBudWxsID0gbnVsbDtcblxuLyoqXG4gKiBgU0xBQ0tfRVhQSVJZX1JFTUlOREVSX0lOVEVSVkFMX01JTlVURVNgIOqwgCDslpHsiJjsnbwg65WM66eMIOy8nOynhOuLpC4g66+47ISk7KCVwrcwIOydtOuptCDruYTtmZzshLEuXG4gKlxuICog7J247Iqk7YS07Iqk6rCAIOyXrOufrCDqsJzrqbQg6rCB7J6QIOuztOuCuOuLpC4g66eM66OMIOyInOqwhCDslYzrprzqs7wg64us66asIOyDge2DnCDrs4DtmZTqsIAg7JeG7Ja0IERCIOuhnCDslrXsoJztlaAg7IiYXG4gKiDsl4bqs6AsIO2VmOujqCDrqocg6rG0IOyImOykgOydtOudvCDspJHrs7XsnYQg6rCQ7IiY7ZWY64qUIO2OuOydtCDsnqDquIgg7J6l7LmY66W8IOuRkOuKlCDqsoPrs7Tri6Qg64Kr64ukLlxuICovXG5leHBvcnQgZnVuY3Rpb24gc3RhcnRFeHBpcmVkVG9rZW5SZW1pbmRlcigpOiB2b2lkIHtcbiAgY29uc3QgbWludXRlcyA9IE51bWJlcihwcm9jZXNzLmVudi5TTEFDS19FWFBJUllfUkVNSU5ERVJfSU5URVJWQUxfTUlOVVRFUyA/PyAwKTtcbiAgaWYgKCFOdW1iZXIuaXNGaW5pdGUobWludXRlcykgfHwgbWludXRlcyA8PSAwKSByZXR1cm47XG5cbiAgY29uc3QgdXNlck1hcCA9IGdldFNsYWNrVXNlck1hcCgpO1xuICBsb2dnZXIuaW5mbyhgZXhwaXJlZCB0b2tlbiByZW1pbmRlciBldmVyeSAke21pbnV0ZXN9bSAoJHt1c2VyTWFwLnNpemV9IHVzZXIgbWFwcGluZ3MpYCk7XG5cbiAgY29uc3QgcnVuID0gKCkgPT5cbiAgICBzZW5kUmVtaW5kZXIodXNlck1hcCkuY2F0Y2goKGUpID0+IGxvZ2dlci53YXJuKGByZW1pbmRlciBmYWlsZWQ6ICR7KGUgYXMgRXJyb3IpLm1lc3NhZ2V9YCkpO1xuXG4gIC8vIOq4sOuPmSDsp4Htm4Tsl5Drj4Qg7ZWcIOuyiCDrs7Trgrjri6QuIOyerOq4sOuPmeydgCDrsLDtj6zrgpgg7J6l7JWgIOuzteq1rCDrlYwg7J287Ja064KY64qU642wLCDqt7gg7Iuc7KCQ7JeQIOydtOuvuFxuICAvLyDrp4zro4zrkJwg7Yag7YGw7J20IOyeiOycvOuptCDssqsg7KO86riw6rCAIOuPjCDrlYzquYzsp4Ag7KGw7Jqp7ZWcIOqyg+ydtCDsnbTsg4HtlZjri6QuXG4gIHZvaWQgcnVuKCk7XG5cbiAgdGltZXIgPSBzZXRJbnRlcnZhbChydW4sIG1pbnV0ZXMgKiBNSU5VVEVfTVMpO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gc3RvcEV4cGlyZWRUb2tlblJlbWluZGVyKCk6IHZvaWQge1xuICBpZiAoIXRpbWVyKSByZXR1cm47XG4gIGNsZWFySW50ZXJ2YWwodGltZXIpO1xuICB0aW1lciA9IG51bGw7XG59XG4iXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7Ozs7Ozs7O0FBa0JBLE1BQU0sU0FBUyxVQUFVLENBQUMsU0FBUyxrQkFBa0IsQ0FBQztBQUV0RCxNQUFNLFlBQVk7Ozs7O0FBTWxCLFNBQWdCLHFCQUNkLFFBQ0EsU0FDUTtDQUNSLE1BQU0sWUFBWSxPQUFPLFNBQVM7Q0FPbEMsT0FBTyxDQUFDLEdBTk0sT0FBTyxLQUFLLFVBQVU7RUFDbEMsTUFBTSxVQUFVLFdBQVcsTUFBTSxNQUFNLE9BQU87RUFDOUMsTUFBTSxRQUFRLE1BQU0sUUFBUTtFQUM1QixJQUFJLENBQUMsV0FBVyxPQUFPO0VBQ3ZCLE9BQU8sVUFBVSxHQUFHLFFBQVEsR0FBRyxVQUFVO0NBQzNDLENBQ1csR0FBTyxhQUFhLENBQUMsQ0FBQyxPQUFPLE9BQU8sQ0FBQyxDQUFDLEtBQUssSUFBSTtBQUM1RDtBQUVBLGVBQWUsYUFBYSxTQUE2QztDQUN2RSxNQUFNLFdBQVcsTUFBTSxXQUFXLGFBQWEsR0FBRztDQUNsRCxJQUFJLFNBQVMsV0FBVyxHQUFHO0NBRTNCLE1BQU0sWUFBWTtFQUVoQixPQUFPO0VBQ1AsU0FBUyxTQUFTLFNBQVMsSUFBSSxHQUFHLFNBQVMsT0FBTyxLQUFNLFNBQVMsRUFBRSxDQUFFLFFBQVE7RUFDN0UsU0FBUyxxQkFBcUIsVUFBVSxPQUFPO0VBQy9DLE9BQU8sWUFBWTtDQUNyQixDQUFDO0FBQ0g7QUFFQSxJQUFJLFFBQStDOzs7Ozs7O0FBUW5ELFNBQWdCLDRCQUFrQztDQUNoRCxNQUFNLFVBQVUsT0FBTyxRQUFRLElBQUksMENBQTBDLENBQUM7Q0FDOUUsSUFBSSxDQUFDLE9BQU8sU0FBUyxPQUFPLEtBQUssV0FBVyxHQUFHO0NBRS9DLE1BQU0sVUFBVSxnQkFBZ0I7Q0FDaEMsT0FBTyxLQUFLLGdDQUFnQyxRQUFRLEtBQUssUUFBUSxLQUFLLGdCQUFnQjtDQUV0RixNQUFNLFlBQ0osYUFBYSxPQUFPLENBQUMsQ0FBQyxPQUFPLE1BQU0sT0FBTyxLQUFLLG9CQUFxQixFQUFZLFNBQVMsQ0FBQztDQUk1RixBQUFLLElBQUk7Q0FFVCxRQUFRLFlBQVksS0FBSyxVQUFVLFNBQVM7QUFDOUM7QUFFQSxTQUFnQiwyQkFBaUM7Q0FDL0MsSUFBSSxDQUFDLE9BQU87Q0FDWixjQUFjLEtBQUs7Q0FDbkIsUUFBUTtBQUNWIn0=
|
|
@@ -8,22 +8,22 @@ const userMap = new Map([
|
|
|
8
8
|
]);
|
|
9
9
|
|
|
10
10
|
describe("buildReminderContext", () => {
|
|
11
|
-
it("
|
|
11
|
+
it("1건이면 제목에 있는 토큰명을 본문에서 반복하지 않는다", () => {
|
|
12
12
|
const text = buildReminderContext([{ name: "anthropic/yds", provider: "anthropic" }], userMap);
|
|
13
13
|
|
|
14
|
-
expect(text).toBe("<@U09NAJUQSFQ
|
|
14
|
+
expect(text).toBe("<@U09NAJUQSFQ>\n재로그인이 필요합니다");
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
-
it("
|
|
17
|
+
it("1건이고 매핑이 없으면 안내만 남는다", () => {
|
|
18
18
|
const text = buildReminderContext(
|
|
19
19
|
[{ name: "anthropic/dev-common", provider: "anthropic" }],
|
|
20
20
|
userMap,
|
|
21
21
|
);
|
|
22
22
|
|
|
23
|
-
expect(text).toBe("
|
|
23
|
+
expect(text).toBe("재로그인이 필요합니다");
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
it("
|
|
26
|
+
it("여러 건이면 제목이 건수라 본문에 토큰명을 남긴다", () => {
|
|
27
27
|
const text = buildReminderContext(
|
|
28
28
|
[
|
|
29
29
|
{ name: "anthropic/yds", provider: "anthropic" },
|
|
@@ -32,22 +32,42 @@ describe("buildReminderContext", () => {
|
|
|
32
32
|
userMap,
|
|
33
33
|
);
|
|
34
34
|
|
|
35
|
-
// 멘션만 두 번 나오면 어느 토큰인지 알 수 없다 — 토큰명이 함께 있어야 한다.
|
|
36
35
|
expect(text.split("\n")).toEqual([
|
|
37
36
|
"<@U09NAJUQSFQ> anthropic/yds",
|
|
38
37
|
"<@U09NAJUQSFQ> openai/yds",
|
|
38
|
+
"재로그인이 필요합니다",
|
|
39
39
|
]);
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
-
it("
|
|
43
|
-
const text = buildReminderContext(
|
|
42
|
+
it("여러 건 중 매핑 없는 공용 계정은 이름만 남긴다", () => {
|
|
43
|
+
const text = buildReminderContext(
|
|
44
|
+
[
|
|
45
|
+
{ name: "anthropic/haze", provider: "anthropic" },
|
|
46
|
+
{ name: "anthropic/dev-common", provider: "anthropic" },
|
|
47
|
+
],
|
|
48
|
+
userMap,
|
|
49
|
+
);
|
|
44
50
|
|
|
45
|
-
expect(text
|
|
51
|
+
expect(text.split("\n")).toEqual([
|
|
52
|
+
"<@U09N96NHZB7> anthropic/haze",
|
|
53
|
+
"anthropic/dev-common",
|
|
54
|
+
"재로그인이 필요합니다",
|
|
55
|
+
]);
|
|
46
56
|
});
|
|
47
57
|
|
|
48
58
|
it("이름이 없는 토큰도 줄을 잃지 않는다", () => {
|
|
49
|
-
const text = buildReminderContext(
|
|
59
|
+
const text = buildReminderContext(
|
|
60
|
+
[
|
|
61
|
+
{ name: null, provider: "openai" },
|
|
62
|
+
{ name: "anthropic/haze", provider: "anthropic" },
|
|
63
|
+
],
|
|
64
|
+
userMap,
|
|
65
|
+
);
|
|
50
66
|
|
|
51
|
-
expect(text
|
|
67
|
+
expect(text.split("\n")).toEqual([
|
|
68
|
+
"unnamed",
|
|
69
|
+
"<@U09N96NHZB7> anthropic/haze",
|
|
70
|
+
"재로그인이 필요합니다",
|
|
71
|
+
]);
|
|
52
72
|
});
|
|
53
73
|
});
|
|
@@ -20,18 +20,22 @@ const logger = getLogger(["qgrid", "expired-reminder"]);
|
|
|
20
20
|
|
|
21
21
|
const MINUTE_MS = 60_000;
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* 1건이면 제목(subject)에 토큰명이 이미 있으므로 본문에서는 뺀다 — 같은 이름을 두 번 쓰면
|
|
25
|
+
* 읽는 사람이 다른 정보인 줄 알고 한 번 더 본다. 여러 건이면 제목이 건수라 본문에 남긴다.
|
|
26
|
+
*/
|
|
23
27
|
export function buildReminderContext(
|
|
24
28
|
tokens: { name: string | null; provider: string }[],
|
|
25
29
|
userMap: Map<string, string>,
|
|
26
30
|
): string {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
const showLabel = tokens.length > 1;
|
|
32
|
+
const lines = tokens.map((token) => {
|
|
33
|
+
const mention = mentionFor(token.name, userMap);
|
|
34
|
+
const label = token.name ?? "unnamed";
|
|
35
|
+
if (!showLabel) return mention;
|
|
36
|
+
return mention ? `${mention} ${label}` : label;
|
|
37
|
+
});
|
|
38
|
+
return [...lines, "재로그인이 필요합니다"].filter(Boolean).join("\n");
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
async function sendReminder(userMap: Map<string, string>): Promise<void> {
|
|
@@ -42,7 +46,7 @@ async function sendReminder(userMap: Map<string, string>): Promise<void> {
|
|
|
42
46
|
// 만료 순간 알림과 같은 제목을 쓴다. 같은 사건이므로 다른 이름을 붙이면 별개 문제로 읽힌다.
|
|
43
47
|
title: "세션 만료",
|
|
44
48
|
subject: inactive.length > 1 ? `${inactive.length}건` : (inactive[0]!.name ?? "unnamed"),
|
|
45
|
-
context:
|
|
49
|
+
context: buildReminderContext(inactive, userMap),
|
|
46
50
|
color: SLACK_COLOR.bad,
|
|
47
51
|
});
|
|
48
52
|
}
|
|
@@ -62,9 +66,14 @@ export function startExpiredTokenReminder(): void {
|
|
|
62
66
|
const userMap = getSlackUserMap();
|
|
63
67
|
logger.info(`expired token reminder every ${minutes}m (${userMap.size} user mappings)`);
|
|
64
68
|
|
|
65
|
-
|
|
69
|
+
const run = () =>
|
|
66
70
|
sendReminder(userMap).catch((e) => logger.warn(`reminder failed: ${(e as Error).message}`));
|
|
67
|
-
|
|
71
|
+
|
|
72
|
+
// 기동 직후에도 한 번 보낸다. 재기동은 배포나 장애 복구 때 일어나는데, 그 시점에 이미
|
|
73
|
+
// 만료된 토큰이 있으면 첫 주기가 돌 때까지 조용한 것이 이상하다.
|
|
74
|
+
void run();
|
|
75
|
+
|
|
76
|
+
timer = setInterval(run, minutes * MINUTE_MS);
|
|
68
77
|
}
|
|
69
78
|
|
|
70
79
|
export function stopExpiredTokenReminder(): void {
|