@cartanova/qgrid-cli 2.6.2 → 2.6.5
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/README.ko.md +2 -0
- package/README.md +2 -0
- package/bundle/dist/application/qgrid/expired-token-digest.js +78 -0
- package/bundle/dist/application/qgrid/qgrid.dispatcher.js +40 -6
- package/bundle/dist/application/qgrid/qgrid.frame.js +22 -7
- package/bundle/dist/application/qgrid/qgrid.types.js +13 -3
- package/bundle/dist/application/qgrid/token-death.js +29 -4
- package/bundle/dist/application/queries.generated.js +2 -1
- package/bundle/dist/application/request-log/request-log.model.js +37 -5
- package/bundle/dist/application/request-log-step/request-log-step.model.js +1 -1
- package/bundle/dist/application/sonamu.generated.js +41 -3
- package/bundle/dist/application/token/token.model.js +44 -14
- package/bundle/dist/i18n/en.js +3 -1
- package/bundle/dist/i18n/ko.js +3 -1
- package/bundle/dist/i18n/sd.generated.js +18 -1
- package/bundle/dist/migrations/20260807164057_alter_request_logs_alter1.js +13 -0
- package/bundle/dist/server-error-handler.js +5 -3
- package/bundle/dist/sonamu.config.js +8 -1
- package/bundle/dist/utils/slack-notify.js +2 -2
- package/bundle/src/application/qgrid/expired-token-digest.test.ts +49 -0
- package/bundle/src/application/qgrid/expired-token-digest.ts +93 -0
- package/bundle/src/application/qgrid/qgrid-oauth-redirect.test.ts +0 -3
- package/bundle/src/application/qgrid/qgrid.dispatcher.test.ts +30 -2
- package/bundle/src/application/qgrid/qgrid.dispatcher.ts +44 -6
- package/bundle/src/application/qgrid/qgrid.frame.ts +13 -4
- package/bundle/src/application/qgrid/qgrid.types.ts +10 -0
- package/bundle/src/application/qgrid/token-death.test.ts +33 -3
- package/bundle/src/application/qgrid/token-death.ts +32 -3
- package/bundle/src/application/queries.generated.ts +3 -0
- package/bundle/src/application/request-log/request-log.entity.json +26 -1
- package/bundle/src/application/request-log/request-log.model.ts +50 -10
- package/bundle/src/application/sonamu.generated.http +6 -0
- package/bundle/src/application/sonamu.generated.ts +42 -2
- package/bundle/src/application/token/token.model.test.ts +13 -3
- package/bundle/src/application/token/token.model.ts +34 -17
- package/bundle/src/i18n/en.ts +3 -0
- package/bundle/src/i18n/ko.ts +3 -0
- package/bundle/src/i18n/sd.generated.ts +17 -0
- package/bundle/src/migrations/20260807164057_alter_request_logs_alter1.ts +13 -0
- package/bundle/src/server-error-handler.test.ts +58 -0
- package/bundle/src/server-error-handler.ts +16 -5
- package/bundle/src/sonamu.config.ts +13 -0
- package/bundle/src/utils/slack-notify.test.ts +6 -4
- package/bundle/src/utils/slack-notify.ts +10 -3
- package/bundle/web-dist/client/assets/index-BGQSGMcK.css +1 -0
- package/bundle/web-dist/client/assets/index-DTy-s79t.js +78 -0
- package/bundle/web-dist/client/assets/logs-DYqUSqg-.js +1 -0
- package/bundle/web-dist/client/assets/{monit-BSa5lSZp.js → monit-GnFS0_Xa.js} +1 -1
- package/bundle/web-dist/client/assets/routes-CBwpn4d-.js +5 -0
- package/bundle/web-dist/client/assets/{sd.generated-CjW3rfJ8.js → sd.generated-DYdwSB2I.js} +1 -1
- package/bundle/web-dist/client/assets/{services.generated-B-dMUtEu.js → services.generated-BsAvotjG.js} +1 -1
- package/bundle/web-dist/client/assets/{show-C1jEPhuS.js → show-DDgukGuY.js} +3 -3
- package/bundle/web-dist/client/assets/{tokens-1e66MAC0.js → tokens-Co1wPrXX.js} +1 -1
- package/bundle/web-dist/client/assets/{use-update-token-mutation-DueEppqt.js → use-update-token-mutation-DqEpK7It.js} +1 -1
- package/bundle/web-dist/client/index.html +3 -3
- package/bundle/web-dist/server/assets/{lazyRouteComponent-PRxXWW3t.js → lazyRouteComponent-BaZaveNc.js} +1 -1
- package/bundle/web-dist/server/assets/{logs-CwoOsveV.js → logs-DMOw3SjD.js} +82 -40
- package/bundle/web-dist/server/assets/{monit-DIsEsDJN.js → monit-UmcqHroX.js} +2 -2
- package/bundle/web-dist/server/assets/{routes-Bts-KJ9W.js → routes-Sv_4FjtH.js} +47 -45
- package/bundle/web-dist/server/assets/{sd.generated-ek-tz-7H.js → sd.generated-B0xqnHEo.js} +21 -0
- package/bundle/web-dist/server/assets/{services.generated-Bh7Vs8Ds.js → services.generated-DqAKskAS.js} +16 -1
- package/bundle/web-dist/server/assets/{show-DJfaCNUw.js → show-C-oSdzHX.js} +31 -25
- package/bundle/web-dist/server/assets/{tokens-BA4vwDat.js → tokens-h-2A3wfD.js} +112 -109
- package/bundle/web-dist/server/assets/{use-update-token-mutation-BQRzEPy2.js → use-update-token-mutation-DHCaa7Ku.js} +1 -1
- package/bundle/web-dist/server/entry-server.generated.js +70 -29
- package/package.json +1 -1
- package/skills/qgrid/references/cli-env-and-server-boot.md +8 -0
- package/skills/qgrid/references/token-auth-quota-lifecycle.md +3 -2
- package/bundle/web-dist/client/assets/index-2cAgaLOm.css +0 -1
- package/bundle/web-dist/client/assets/index-BgWzzjvj.js +0 -78
- package/bundle/web-dist/client/assets/logs-Lyv8OfHE.js +0 -1
- package/bundle/web-dist/client/assets/routes-Cp0re-Nn.js +0 -5
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { monitLogSink } from "./application/monit/log-buffer.js";
|
|
2
2
|
import { QgridDispatcher } from "./application/qgrid/qgrid.dispatcher.js";
|
|
3
|
+
import { startExpiredTokenDigest, stopExpiredTokenDigest } from "./application/qgrid/expired-token-digest.js";
|
|
3
4
|
import { QgridFrame } from "./application/qgrid/qgrid.frame.js";
|
|
4
5
|
import { TokenSubscriber } from "./application/qgrid/token-subscriber.js";
|
|
5
6
|
import { ensureTokensTrigger } from "./application/qgrid/token-trigger-setup.js";
|
|
@@ -146,16 +147,21 @@ var sonamu_config_default = defineConfig({
|
|
|
146
147
|
const openaiDispatcher = new OpenAIDispatcher();
|
|
147
148
|
await openaiDispatcher.start();
|
|
148
149
|
QgridDispatcher.openaiDispatcher = openaiDispatcher;
|
|
150
|
+
QgridDispatcher.startupState.openai = "ready";
|
|
149
151
|
} catch (e) {
|
|
152
|
+
QgridDispatcher.startupState.openai = "failed";
|
|
150
153
|
log.warn(`openai dispatcher failed: ${e.message}`);
|
|
151
154
|
}
|
|
152
155
|
try {
|
|
153
156
|
const anthropicDispatcher = new AnthropicDispatcher();
|
|
154
157
|
await anthropicDispatcher.start();
|
|
155
158
|
QgridDispatcher.anthropicDispatcher = anthropicDispatcher;
|
|
159
|
+
QgridDispatcher.startupState.anthropic = "ready";
|
|
156
160
|
} catch (e) {
|
|
161
|
+
QgridDispatcher.startupState.anthropic = "failed";
|
|
157
162
|
log.warn(`anthropic dispatcher failed: ${e.message}`);
|
|
158
163
|
}
|
|
164
|
+
startExpiredTokenDigest();
|
|
159
165
|
const anthropicCount = QgridDispatcher.anthropicDispatcher?.tokenCount ?? 0;
|
|
160
166
|
const openaiReady = QgridDispatcher.openaiDispatcher?.readyWorkerCount ?? 0;
|
|
161
167
|
const openaiTotal = QgridDispatcher.openaiDispatcher?.workerCount ?? 0;
|
|
@@ -166,6 +172,7 @@ var sonamu_config_default = defineConfig({
|
|
|
166
172
|
},
|
|
167
173
|
onShutdown: async () => {
|
|
168
174
|
const log = getLogger(["qgrid", "startup"]);
|
|
175
|
+
stopExpiredTokenDigest();
|
|
169
176
|
if (QgridDispatcher.openaiDispatcher) await QgridDispatcher.openaiDispatcher.stop();
|
|
170
177
|
if (QgridDispatcher.anthropicDispatcher) await QgridDispatcher.anthropicDispatcher.stop();
|
|
171
178
|
if (QgridDispatcher.subscriber) await QgridDispatcher.subscriber.stop();
|
|
@@ -180,4 +187,4 @@ var sonamu_config_default = defineConfig({
|
|
|
180
187
|
//#endregion
|
|
181
188
|
export { sonamu_config_default as default };
|
|
182
189
|
|
|
183
|
-
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic29uYW11LmNvbmZpZy5qcyIsIm5hbWVzIjpbImNhY2hlRHJpdmVycyJdLCJzb3VyY2VzIjpbIi4uL3NyYy9zb25hbXUuY29uZmlnLnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBwYXRoIGZyb20gXCJwYXRoXCI7XG5cbmltcG9ydCB7IGdldENvbnNvbGVTaW5rIH0gZnJvbSBcIkBsb2d0YXBlL2xvZ3RhcGVcIjtcbmltcG9ydCB7IGdldExvZ2dlciB9IGZyb20gXCJAbG9ndGFwZS9sb2d0YXBlXCI7XG5pbXBvcnQgeyBnZXRQcmV0dHlGb3JtYXR0ZXIgfSBmcm9tIFwiQGxvZ3RhcGUvcHJldHR5XCI7XG5pbXBvcnQgeyBDYWNoZVByZXNldHMsIGRlZmluZUNvbmZpZyB9IGZyb20gXCJzb25hbXVcIjtcbmltcG9ydCB7IGRyaXZlcnMgYXMgY2FjaGVEcml2ZXJzLCBzdG9yZSB9IGZyb20gXCJzb25hbXUvY2FjaGVcIjtcblxuaW1wb3J0IHsgbW9uaXRMb2dTaW5rIH0gZnJvbSBcIi4vYXBwbGljYXRpb24vbW9uaXQvbG9nLWJ1ZmZlclwiO1xuaW1wb3J0IHsgUWdyaWREaXNwYXRjaGVyIH0gZnJvbSBcIi4vYXBwbGljYXRpb24vcWdyaWQvcWdyaWQuZGlzcGF0Y2hlclwiO1xuaW1wb3J0IHsgUWdyaWRGcmFtZSB9IGZyb20gXCIuL2FwcGxpY2F0aW9uL3FncmlkL3FncmlkLmZyYW1lXCI7XG5pbXBvcnQgeyBUb2tlblN1YnNjcmliZXIgfSBmcm9tIFwiLi9hcHBsaWNhdGlvbi9xZ3JpZC90b2tlbi1zdWJzY3JpYmVyXCI7XG5pbXBvcnQgeyBlbnN1cmVUb2tlbnNUcmlnZ2VyIH0gZnJvbSBcIi4vYXBwbGljYXRpb24vcWdyaWQvdG9rZW4tdHJpZ2dlci1zZXR1cFwiO1xuaW1wb3J0IHsgaGFuZGxlU2VydmVyRXJyb3IgfSBmcm9tIFwiLi9zZXJ2ZXItZXJyb3ItaGFuZGxlclwiO1xuaW1wb3J0IHsgQW50aHJvcGljRGlzcGF0Y2hlciB9IGZyb20gXCIuL3V0aWxzL3Byb3ZpZGVycy9hbnRocm9waWMvYW50aHJvcGljLWRpc3BhdGNoZXJcIjtcbmltcG9ydCB7IE9wZW5BSURpc3BhdGNoZXIgfSBmcm9tIFwiLi91dGlscy9wcm92aWRlcnMvb3BlbmFpL29wZW5haS1kaXNwYXRjaGVyXCI7XG5cbmNvbnN0IGhvc3QgPSBwcm9jZXNzLmVudi5IT1NUID8/IFwibG9jYWxob3N0XCI7XG5jb25zdCBwb3J0ID0gTnVtYmVyKHByb2Nlc3MuZW52LlBPUlQgPz8gNDQ5MDApO1xuXG5jb25zdCBjb25uQ29uZmlnID0ge1xuICBob3N0OiBwcm9jZXNzLmVudi5TT05BTVVfREJfSE9TVCA/PyBcImxvY2FsaG9zdFwiLFxuICBwb3J0OiBOdW1iZXIocHJvY2Vzcy5lbnYuU09OQU1VX0RCX1BPUlQgPz8gNTQzMiksXG4gIHVzZXI6IHByb2Nlc3MuZW52LlNPTkFNVV9EQl9VU0VSID8/IFwicG9zdGdyZXNcIixcbiAgcGFzc3dvcmQ6IHByb2Nlc3MuZW52LlNPTkFNVV9EQl9QQVNTV09SRCA/PyBcInBvc3RncmVzXCIsXG4gIGRhdGFiYXNlOiBwcm9jZXNzLmVudi5TT05BTVVfREJfTkFNRSA/PyBcInFncmlkXCIsXG59O1xuXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICBwcm9qZWN0TmFtZTogcHJvY2Vzcy5lbnYuUFJPSkVDVF9OQU1FID8/IFwiUWdyaWRcIixcbiAgZGF0YWJhc2U6IHtcbiAgICBkZWZhdWx0T3B0aW9uczoge1xuICAgICAgY29ubmVjdGlvbjoge1xuICAgICAgICAuLi5jb25uQ29uZmlnLFxuICAgICAgICBrZWVwQWxpdmU6IHRydWUsXG4gICAgICAgIGtlZXBBbGl2ZUluaXRpYWxEZWxheU1pbGxpczogMTAwMDAsXG4gICAgICB9LFxuICAgIH0sXG4gIH0sXG4gIGFwaToge1xuICAgIGRpcjogXCJhcGlcIixcbiAgICB0aW1lem9uZTogXCJBc2lhL1Nlb3VsXCIsXG4gICAgcm91dGU6IHtcbiAgICAgIHByZWZpeDogXCIvYXBpXCIsXG4gICAgfSxcbiAgfSxcbiAgaTE4bjoge1xuICAgIGRlZmF1bHRMb2NhbGU6IFwia29cIixcbiAgICBzdXBwb3J0ZWRMb2NhbGVzOiBbXCJrb1wiLCBcImVuXCJdLFxuICB9LFxuICBzeW5jOiB7XG4gICAgdGFyZ2V0czogW1wid2ViXCJdLFxuICB9LFxuICB0ZXN0OiB7XG4gICAgcGFyYWxsZWw6IHRydWUsXG4gICAgbWF4V29ya2VyczogNCxcbiAgICBkZXZSdW5uZXI6IHsgZW5hYmxlZDogdHJ1ZSB9LFxuICB9LFxuICBsb2dnaW5nOiB7XG4gICAgc2lua3M6IHtcbiAgICAgIGNvbnNvbGU6IGdldENvbnNvbGVTaW5rKHtcbiAgICAgICAgZm9ybWF0dGVyOiBnZXRQcmV0dHlGb3JtYXR0ZXIoe1xuICAgICAgICAgIHRpbWVzdGFtcDogXCJ0aW1lXCIsXG4gICAgICAgICAgY2F0ZWdvcnlXaWR0aDogMjAsXG4gICAgICAgICAgY2F0ZWdvcnlUcnVuY2F0ZTogXCJtaWRkbGVcIixcbiAgICAgICAgfSksXG4gICAgICB9KSxcbiAgICAgIC8vIE1vbml0IO2DreyaqSBpbi1tZW1vcnkgcmluZyBidWZmZXIuIO2PrOunt+ydgCBzaW5rIOuzhCDigJQg7L2Y7IaUIOy2nOugpeydgCDqt7jrjIDroZzri6QuXG4gICAgICBtb25pdDogbW9uaXRMb2dTaW5rLFxuICAgIH0sXG4gICAgbG9nZ2VyczogW1xuICAgICAgLy8gcWdyaWQg66Gc6re4ICjsmpTssq0g67aE67CwLCBvYXV0aC91c2FnZSBBUEkpXG4gICAgICB7IGNhdGVnb3J5OiBbXCJxZ3JpZFwiXSwgc2lua3M6IFtcImNvbnNvbGVcIiwgXCJtb25pdFwiXSwgbG93ZXN0TGV2ZWw6IFwiaW5mb1wiIH0sXG4gICAgICAvLyBzb25hbXUg66Gc6re464qUIHdhcm5pbmcg7J207IOB66eMXG4gICAgICB7IGNhdGVnb3J5OiBbXCJzb25hbXVcIl0sIHNpbmtzOiBbXCJjb25zb2xlXCIsIFwibW9uaXRcIl0sIGxvd2VzdExldmVsOiBcIndhcm5pbmdcIiB9LFxuICAgICAgLy8gZmFzdGlmeSDroZzqt7gg7LCo64uoXG4gICAgICB7IGNhdGVnb3J5OiBbXCJmYXN0aWZ5XCJdLCBzaW5rczogW10sIGxvd2VzdExldmVsOiBcImZhdGFsXCIgfSxcbiAgICBdLFxuICB9LFxuICBzZXJ2ZXI6IHtcbiAgICBsaXN0ZW46IHsgcG9ydCwgaG9zdCB9LFxuICAgIGZhc3RpZnk6IHtcbiAgICAgIGJvZHlMaW1pdDogMTAgKiAxMDI0ICogMTAyNCxcbiAgICB9LFxuICAgIHBsdWdpbnM6IHtcbiAgICAgIGZvcm1ib2R5OiB0cnVlLFxuICAgICAgcXM6IHRydWUsXG4gICAgICBzc2U6IHRydWUsXG4gICAgICBtdWx0aXBhcnQ6IHsgbGltaXRzOiB7IGZpbGVTaXplOiAxMDI0ICogMTAyNCAqIDMwIH0gfSxcbiAgICAgIHN0YXRpYzoge1xuICAgICAgICByb290OiBwYXRoLmpvaW4oaW1wb3J0Lm1ldGEuZGlybmFtZSwgXCIvLi4vXCIsIFwicHVibGljXCIpLFxuICAgICAgICBwcmVmaXg6IFwiL2FwaS9wdWJsaWNcIixcbiAgICAgIH0sXG4gICAgICBjdXN0b206IChzZXJ2ZXIpID0+IHtcbiAgICAgICAgLy8gT0F1dGgg7L2c67CxIOKAlCBBbnRocm9waWPsnbQgL2NhbGxiYWNr7Jy866GcIOumrOuLpOydtOugie2KuFxuICAgICAgICBzZXJ2ZXIuZ2V0KFwiL2NhbGxiYWNrXCIsIGFzeW5jIChyZXF1ZXN0LCByZXBseSkgPT4ge1xuICAgICAgICAgIGNvbnN0IHsgY29kZSwgc3RhdGUgfSA9IHJlcXVlc3QucXVlcnkgYXMgeyBjb2RlPzogc3RyaW5nOyBzdGF0ZT86IHN0cmluZyB9O1xuICAgICAgICAgIGlmICghY29kZSB8fCAhc3RhdGUpIHtcbiAgICAgICAgICAgIHJldHVybiByZXBseS5yZWRpcmVjdChcIi8/b2F1dGg9ZXJyb3ImcmVhc29uPW1pc3NpbmdfcGFyYW1zXCIpO1xuICAgICAgICAgIH1cbiAgICAgICAgICB0cnkge1xuICAgICAgICAgICAgcmV0dXJuIFFncmlkRnJhbWUuaGFuZGxlT0F1dGhDYWxsYmFjayhjb2RlLCBzdGF0ZSwgcmVwbHkpO1xuICAgICAgICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgICAgIHJldHVybiByZXBseS5yZWRpcmVjdChcbiAgICAgICAgICAgICAgYC8/b2F1dGg9ZXJyb3ImcmVhc29uPSR7ZW5jb2RlVVJJQ29tcG9uZW50KChlIGFzIEVycm9yKS5tZXNzYWdlKX1gLFxuICAgICAgICAgICAgKTtcbiAgICAgICAgICB9XG4gICAgICAgIH0pO1xuXG4gICAgICAgIC8vIE9wZW5BSSBPQXV0aCDripQgY29kZXggYXBwLXNlcnZlciDqsIAg7J6Q7LK0IGNhbGxiYWNrIOyEnOuyhOulvCDsmKzrprxcbiAgICAgICAgLy8gaGFuZGxlT3BlbkFJQ2FsbGJhY2sg67aI7ZWE7JqUIOKAlCBvYXV0aENvbXBsZXRlT3BlbkFJIEFQSSDroZwg7JmE66OMIO2ZleyduFxuICAgICAgfSxcbiAgICB9LFxuICAgIGFwaUNvbmZpZzoge1xuICAgICAgY29udGV4dFByb3ZpZGVyOiAoZGVmYXVsdENvbnRleHQsIHJlcXVlc3QpID0+IHtcbiAgICAgICAgcmV0dXJuIHtcbiAgICAgICAgICAuLi5kZWZhdWx0Q29udGV4dCxcbiAgICAgICAgICBpcDogcmVxdWVzdC5pcCxcbiAgICAgICAgICBib2R5OiByZXF1ZXN0LmJvZHksXG4gICAgICAgIH07XG4gICAgICB9LFxuICAgICAgZ3VhcmRIYW5kbGVyOiAoKSA9PiB7fSxcbiAgICAgIGNhY2hlQ29udHJvbEhhbmRsZXI6IChyZXEpID0+IHtcbiAgICAgICAgc3dpdGNoIChyZXEudHlwZSkge1xuICAgICAgICAgIGNhc2UgXCJhc3NldHNcIjpcbiAgICAgICAgICAgIC8vIEhhc2gg7Y+s7ZWo65CcIO2MjOydvDog7JiB6rWsIOy6kOyLnFxuICAgICAgICAgICAgaWYgKHJlcS5wYXRoLm1hdGNoKC8tW2EtZjAtOV0rXFwuLykpIHtcbiAgICAgICAgICAgICAgcmV0dXJuIENhY2hlUHJlc2V0cy5pbW11dGFibGU7XG4gICAgICAgICAgICB9XG4gICAgICAgICAgICByZXR1cm4gQ2FjaGVQcmVzZXRzLmxvbmdMaXZlZDtcblxuICAgICAgICAgIGNhc2UgXCJhcGlcIjpcbiAgICAgICAgICAgIC8vIEdFVCDsmpTssq3rp4wg7LqQ7IuxIOqzoOugpFxuICAgICAgICAgICAgaWYgKHJlcS5tZXRob2QgPT09IFwiR0VUXCIpIHtcbiAgICAgICAgICAgICAgLy8g7Yq57KCVIOqyveuhnOuKlCDsp6fsnYAg7LqQ7IucXG4gICAgICAgICAgICAgIGlmIChyZXEucGF0aC5zdGFydHNXaXRoKFwiL2FwaS9zdGF0aWMtZGF0YVwiKSkge1xuICAgICAgICAgICAgICAgIHJldHVybiBDYWNoZVByZXNldHMuc2hvcnRMaXZlZDtcbiAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICBpZiAocmVxLnBhdGguc3RhcnRzV2l0aChcIi9hcGkvdGVybXNcIikpIHtcbiAgICAgICAgICAgICAgICByZXR1cm4gQ2FjaGVQcmVzZXRzLm1lZGl1bUxpdmVkO1xuICAgICAgICAgICAgICB9XG4gICAgICAgICAgICB9XG4gICAgICAgICAgICAvLyDquLDrs7g6IOy6kOyLnCDsl4bsnYxcbiAgICAgICAgICAgIHJldHVybiBDYWNoZVByZXNldHMubm9DYWNoZTtcblxuICAgICAgICAgIGNhc2UgXCJzc3JcIjpcbiAgICAgICAgICAgIC8vIFNTUiDtjpjsnbTsp4A6IDEw7LSIIOy6kOyLnFxuICAgICAgICAgICAgcmV0dXJuIENhY2hlUHJlc2V0cy5zc3I7XG5cbiAgICAgICAgICBjYXNlIFwiY3NyXCI6XG4gICAgICAgICAgICAvLyBDU1IgZmFsbGJhY2sgKGluZGV4Lmh0bWwpOiAx67aEIOy6kOyLnFxuICAgICAgICAgICAgcmV0dXJuIENhY2hlUHJlc2V0cy5zaG9ydExpdmVkO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiBDYWNoZVByZXNldHMubm9DYWNoZTtcbiAgICAgIH0sXG4gICAgfSxcbiAgICBjYWNoZToge1xuICAgICAgZGVmYXVsdDogXCJtYWluXCIsXG4gICAgICBzdG9yZXM6IHtcbiAgICAgICAgbWFpbjogc3RvcmUoKS51c2VMMUxheWVyKGNhY2hlRHJpdmVycy5tZW1vcnkoeyBtYXhTaXplOiBcIjUwbWJcIiB9KSksXG4gICAgICB9LFxuICAgICAgdHRsOiBcIjVtXCIsXG4gICAgICBwcmVmaXg6IFwiXCIsXG4gICAgfSxcbiAgICBsaWZlY3ljbGU6IHtcbiAgICAgIG9uU3RhcnQ6IGFzeW5jICgpID0+IHtcbiAgICAgICAgY29uc3QgbG9nID0gZ2V0TG9nZ2VyKFtcInFncmlkXCIsIFwic3RhcnR1cFwiXSk7XG5cbiAgICAgICAgbGV0IHRyaWdnZXJSZWFkeSA9IHRydWU7XG4gICAgICAgIHRyeSB7XG4gICAgICAgICAgYXdhaXQgZW5zdXJlVG9rZW5zVHJpZ2dlcihjb25uQ29uZmlnKTtcbiAgICAgICAgfSBjYXRjaCAoZSkge1xuICAgICAgICAgIHRyaWdnZXJSZWFkeSA9IGZhbHNlO1xuICAgICAgICAgIGxvZy53YXJuKGB0cmlnZ2VyIHNldHVwIGZhaWxlZDogJHsoZSBhcyBFcnJvcikubWVzc2FnZX1gKTtcbiAgICAgICAgfVxuXG4gICAgICAgIGNvbnN0IHN1YnNjcmliZXIgPSBuZXcgVG9rZW5TdWJzY3JpYmVyKGNvbm5Db25maWcsIFFncmlkRGlzcGF0Y2hlcik7XG4gICAgICAgIFFncmlkRGlzcGF0Y2hlci5zdWJzY3JpYmVyID0gc3Vic2NyaWJlcjtcblxuICAgICAgICBjb25zdCBzdGFydGVkID0gYXdhaXQgc3Vic2NyaWJlci5zdGFydCgpO1xuXG4gICAgICAgIHRyeSB7XG4gICAgICAgICAgY29uc3Qgb3BlbmFpRGlzcGF0Y2hlciA9IG5ldyBPcGVuQUlEaXNwYXRjaGVyKCk7XG4gICAgICAgICAgYXdhaXQgb3BlbmFpRGlzcGF0Y2hlci5zdGFydCgpO1xuICAgICAgICAgIFFncmlkRGlzcGF0Y2hlci5vcGVuYWlEaXNwYXRjaGVyID0gb3BlbmFpRGlzcGF0Y2hlcjtcbiAgICAgICAgfSBjYXRjaCAoZSkge1xuICAgICAgICAgIGxvZy53YXJuKGBvcGVuYWkgZGlzcGF0Y2hlciBmYWlsZWQ6ICR7KGUgYXMgRXJyb3IpLm1lc3NhZ2V9YCk7XG4gICAgICAgIH1cblxuICAgICAgICB0cnkge1xuICAgICAgICAgIGNvbnN0IGFudGhyb3BpY0Rpc3BhdGNoZXIgPSBuZXcgQW50aHJvcGljRGlzcGF0Y2hlcigpO1xuICAgICAgICAgIGF3YWl0IGFudGhyb3BpY0Rpc3BhdGNoZXIuc3RhcnQoKTtcbiAgICAgICAgICBRZ3JpZERpc3BhdGNoZXIuYW50aHJvcGljRGlzcGF0Y2hlciA9IGFudGhyb3BpY0Rpc3BhdGNoZXI7XG4gICAgICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgICBsb2cud2FybihgYW50aHJvcGljIGRpc3BhdGNoZXIgZmFpbGVkOiAkeyhlIGFzIEVycm9yKS5tZXNzYWdlfWApO1xuICAgICAgICB9XG5cbiAgICAgICAgY29uc3QgYW50aHJvcGljQ291bnQgPSBRZ3JpZERpc3BhdGNoZXIuYW50aHJvcGljRGlzcGF0Y2hlcj8udG9rZW5Db3VudCA/PyAwO1xuICAgICAgICBjb25zdCBvcGVuYWlSZWFkeSA9IFFncmlkRGlzcGF0Y2hlci5vcGVuYWlEaXNwYXRjaGVyPy5yZWFkeVdvcmtlckNvdW50ID8/IDA7XG4gICAgICAgIGNvbnN0IG9wZW5haVRvdGFsID0gUWdyaWREaXNwYXRjaGVyLm9wZW5haURpc3BhdGNoZXI/LndvcmtlckNvdW50ID8/IDA7XG5cbiAgICAgICAgbG9nLmluZm8oYGxpc3RlbmluZyBvbiBodHRwOi8vJHtob3N0fToke3BvcnR9YCk7XG4gICAgICAgIGxvZy5pbmZvKGBhbnRocm9waWM6ICR7YW50aHJvcGljQ291bnR9IHRva2VucyByZWFkeWApO1xuICAgICAgICBsb2cuaW5mbyhgb3BlbmFpOiAke29wZW5haVJlYWR5fS8ke29wZW5haVRvdGFsfSB0b2tlbnMgcmVhZHlgKTtcbiAgICAgICAgbG9nLmluZm8oXG4gICAgICAgICAgYHN1YnNjcmliZXI6ICR7c3RhcnRlZCA/IFwiTElTVEVOIGFjdGl2ZVwiIDogXCJkZWdyYWRlZFwifSR7dHJpZ2dlclJlYWR5ID8gXCJcIiA6IFwiLCB0cmlnZ2VyIGZhaWxlZFwifWAsXG4gICAgICAgICk7XG4gICAgICB9LFxuICAgICAgb25TaHV0ZG93bjogYXN5bmMgKCkgPT4ge1xuICAgICAgICBjb25zdCBsb2cgPSBnZXRMb2dnZXIoW1wicWdyaWRcIiwgXCJzdGFydHVwXCJdKTtcbiAgICAgICAgaWYgKFFncmlkRGlzcGF0Y2hlci5vcGVuYWlEaXNwYXRjaGVyKSB7XG4gICAgICAgICAgYXdhaXQgUWdyaWREaXNwYXRjaGVyLm9wZW5haURpc3BhdGNoZXIuc3RvcCgpO1xuICAgICAgICB9XG4gICAgICAgIGlmIChRZ3JpZERpc3BhdGNoZXIuYW50aHJvcGljRGlzcGF0Y2hlcikge1xuICAgICAgICAgIGF3YWl0IFFncmlkRGlzcGF0Y2hlci5hbnRocm9waWNEaXNwYXRjaGVyLnN0b3AoKTtcbiAgICAgICAgfVxuICAgICAgICBpZiAoUWdyaWREaXNwYXRjaGVyLnN1YnNjcmliZXIpIHtcbiAgICAgICAgICBhd2FpdCBRZ3JpZERpc3BhdGNoZXIuc3Vic2NyaWJlci5zdG9wKCk7XG4gICAgICAgIH1cbiAgICAgICAgbG9nLmluZm8oXCJncmFjZWZ1bCBzaHV0ZG93blwiKTtcbiAgICAgIH0sXG4gICAgICBvbkVycm9yOiAoZXJyb3IsIF9yZXF1ZXN0LCByZXBseSkgPT4ge1xuICAgICAgICBoYW5kbGVTZXJ2ZXJFcnJvcihlcnJvciwgcmVwbHkpO1xuICAgICAgfSxcbiAgICB9LFxuICB9LFxufSk7XG4iXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7Ozs7O0FBaUJBLE1BQU0sT0FBTyxRQUFRLElBQUksUUFBUTtBQUNqQyxNQUFNLE9BQU8sT0FBTyxRQUFRLElBQUksUUFBUSxLQUFLO0FBRTdDLE1BQU0sYUFBYTtDQUNqQixNQUFNLFFBQVEsSUFBSSxrQkFBa0I7Q0FDcEMsTUFBTSxPQUFPLFFBQVEsSUFBSSxr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
|
|
190
|
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic29uYW11LmNvbmZpZy5qcyIsIm5hbWVzIjpbImNhY2hlRHJpdmVycyJdLCJzb3VyY2VzIjpbIi4uL3NyYy9zb25hbXUuY29uZmlnLnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBwYXRoIGZyb20gXCJwYXRoXCI7XG5cbmltcG9ydCB7IGdldENvbnNvbGVTaW5rIH0gZnJvbSBcIkBsb2d0YXBlL2xvZ3RhcGVcIjtcbmltcG9ydCB7IGdldExvZ2dlciB9IGZyb20gXCJAbG9ndGFwZS9sb2d0YXBlXCI7XG5pbXBvcnQgeyBnZXRQcmV0dHlGb3JtYXR0ZXIgfSBmcm9tIFwiQGxvZ3RhcGUvcHJldHR5XCI7XG5pbXBvcnQgeyBDYWNoZVByZXNldHMsIGRlZmluZUNvbmZpZyB9IGZyb20gXCJzb25hbXVcIjtcbmltcG9ydCB7IGRyaXZlcnMgYXMgY2FjaGVEcml2ZXJzLCBzdG9yZSB9IGZyb20gXCJzb25hbXUvY2FjaGVcIjtcblxuaW1wb3J0IHsgbW9uaXRMb2dTaW5rIH0gZnJvbSBcIi4vYXBwbGljYXRpb24vbW9uaXQvbG9nLWJ1ZmZlclwiO1xuaW1wb3J0IHtcbiAgc3RhcnRFeHBpcmVkVG9rZW5EaWdlc3QsXG4gIHN0b3BFeHBpcmVkVG9rZW5EaWdlc3QsXG59IGZyb20gXCIuL2FwcGxpY2F0aW9uL3FncmlkL2V4cGlyZWQtdG9rZW4tZGlnZXN0XCI7XG5pbXBvcnQgeyBRZ3JpZERpc3BhdGNoZXIgfSBmcm9tIFwiLi9hcHBsaWNhdGlvbi9xZ3JpZC9xZ3JpZC5kaXNwYXRjaGVyXCI7XG5pbXBvcnQgeyBRZ3JpZEZyYW1lIH0gZnJvbSBcIi4vYXBwbGljYXRpb24vcWdyaWQvcWdyaWQuZnJhbWVcIjtcbmltcG9ydCB7IFRva2VuU3Vic2NyaWJlciB9IGZyb20gXCIuL2FwcGxpY2F0aW9uL3FncmlkL3Rva2VuLXN1YnNjcmliZXJcIjtcbmltcG9ydCB7IGVuc3VyZVRva2Vuc1RyaWdnZXIgfSBmcm9tIFwiLi9hcHBsaWNhdGlvbi9xZ3JpZC90b2tlbi10cmlnZ2VyLXNldHVwXCI7XG5pbXBvcnQgeyBoYW5kbGVTZXJ2ZXJFcnJvciB9IGZyb20gXCIuL3NlcnZlci1lcnJvci1oYW5kbGVyXCI7XG5pbXBvcnQgeyBBbnRocm9waWNEaXNwYXRjaGVyIH0gZnJvbSBcIi4vdXRpbHMvcHJvdmlkZXJzL2FudGhyb3BpYy9hbnRocm9waWMtZGlzcGF0Y2hlclwiO1xuaW1wb3J0IHsgT3BlbkFJRGlzcGF0Y2hlciB9IGZyb20gXCIuL3V0aWxzL3Byb3ZpZGVycy9vcGVuYWkvb3BlbmFpLWRpc3BhdGNoZXJcIjtcblxuY29uc3QgaG9zdCA9IHByb2Nlc3MuZW52LkhPU1QgPz8gXCJsb2NhbGhvc3RcIjtcbmNvbnN0IHBvcnQgPSBOdW1iZXIocHJvY2Vzcy5lbnYuUE9SVCA/PyA0NDkwMCk7XG5cbmNvbnN0IGNvbm5Db25maWcgPSB7XG4gIGhvc3Q6IHByb2Nlc3MuZW52LlNPTkFNVV9EQl9IT1NUID8/IFwibG9jYWxob3N0XCIsXG4gIHBvcnQ6IE51bWJlcihwcm9jZXNzLmVudi5TT05BTVVfREJfUE9SVCA/PyA1NDMyKSxcbiAgdXNlcjogcHJvY2Vzcy5lbnYuU09OQU1VX0RCX1VTRVIgPz8gXCJwb3N0Z3Jlc1wiLFxuICBwYXNzd29yZDogcHJvY2Vzcy5lbnYuU09OQU1VX0RCX1BBU1NXT1JEID8/IFwicG9zdGdyZXNcIixcbiAgZGF0YWJhc2U6IHByb2Nlc3MuZW52LlNPTkFNVV9EQl9OQU1FID8/IFwicWdyaWRcIixcbn07XG5cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHByb2plY3ROYW1lOiBwcm9jZXNzLmVudi5QUk9KRUNUX05BTUUgPz8gXCJRZ3JpZFwiLFxuICBkYXRhYmFzZToge1xuICAgIGRlZmF1bHRPcHRpb25zOiB7XG4gICAgICBjb25uZWN0aW9uOiB7XG4gICAgICAgIC4uLmNvbm5Db25maWcsXG4gICAgICAgIGtlZXBBbGl2ZTogdHJ1ZSxcbiAgICAgICAga2VlcEFsaXZlSW5pdGlhbERlbGF5TWlsbGlzOiAxMDAwMCxcbiAgICAgIH0sXG4gICAgfSxcbiAgfSxcbiAgYXBpOiB7XG4gICAgZGlyOiBcImFwaVwiLFxuICAgIHRpbWV6b25lOiBcIkFzaWEvU2VvdWxcIixcbiAgICByb3V0ZToge1xuICAgICAgcHJlZml4OiBcIi9hcGlcIixcbiAgICB9LFxuICB9LFxuICBpMThuOiB7XG4gICAgZGVmYXVsdExvY2FsZTogXCJrb1wiLFxuICAgIHN1cHBvcnRlZExvY2FsZXM6IFtcImtvXCIsIFwiZW5cIl0sXG4gIH0sXG4gIHN5bmM6IHtcbiAgICB0YXJnZXRzOiBbXCJ3ZWJcIl0sXG4gIH0sXG4gIHRlc3Q6IHtcbiAgICBwYXJhbGxlbDogdHJ1ZSxcbiAgICBtYXhXb3JrZXJzOiA0LFxuICAgIGRldlJ1bm5lcjogeyBlbmFibGVkOiB0cnVlIH0sXG4gIH0sXG4gIGxvZ2dpbmc6IHtcbiAgICBzaW5rczoge1xuICAgICAgY29uc29sZTogZ2V0Q29uc29sZVNpbmsoe1xuICAgICAgICBmb3JtYXR0ZXI6IGdldFByZXR0eUZvcm1hdHRlcih7XG4gICAgICAgICAgdGltZXN0YW1wOiBcInRpbWVcIixcbiAgICAgICAgICBjYXRlZ29yeVdpZHRoOiAyMCxcbiAgICAgICAgICBjYXRlZ29yeVRydW5jYXRlOiBcIm1pZGRsZVwiLFxuICAgICAgICB9KSxcbiAgICAgIH0pLFxuICAgICAgLy8gTW9uaXQg7YOt7JqpIGluLW1lbW9yeSByaW5nIGJ1ZmZlci4g7Y+s66e37J2AIHNpbmsg67OEIOKAlCDsvZjshpQg7Lac66Cl7J2AIOq3uOuMgOuhnOuLpC5cbiAgICAgIG1vbml0OiBtb25pdExvZ1NpbmssXG4gICAgfSxcbiAgICBsb2dnZXJzOiBbXG4gICAgICAvLyBxZ3JpZCDroZzqt7ggKOyalOyyrSDrtoTrsLAsIG9hdXRoL3VzYWdlIEFQSSlcbiAgICAgIHsgY2F0ZWdvcnk6IFtcInFncmlkXCJdLCBzaW5rczogW1wiY29uc29sZVwiLCBcIm1vbml0XCJdLCBsb3dlc3RMZXZlbDogXCJpbmZvXCIgfSxcbiAgICAgIC8vIHNvbmFtdSDroZzqt7jripQgd2FybmluZyDsnbTsg4Hrp4xcbiAgICAgIHsgY2F0ZWdvcnk6IFtcInNvbmFtdVwiXSwgc2lua3M6IFtcImNvbnNvbGVcIiwgXCJtb25pdFwiXSwgbG93ZXN0TGV2ZWw6IFwid2FybmluZ1wiIH0sXG4gICAgICAvLyBmYXN0aWZ5IOuhnOq3uCDssKjri6hcbiAgICAgIHsgY2F0ZWdvcnk6IFtcImZhc3RpZnlcIl0sIHNpbmtzOiBbXSwgbG93ZXN0TGV2ZWw6IFwiZmF0YWxcIiB9LFxuICAgIF0sXG4gIH0sXG4gIHNlcnZlcjoge1xuICAgIGxpc3RlbjogeyBwb3J0LCBob3N0IH0sXG4gICAgZmFzdGlmeToge1xuICAgICAgYm9keUxpbWl0OiAxMCAqIDEwMjQgKiAxMDI0LFxuICAgIH0sXG4gICAgcGx1Z2luczoge1xuICAgICAgZm9ybWJvZHk6IHRydWUsXG4gICAgICBxczogdHJ1ZSxcbiAgICAgIHNzZTogdHJ1ZSxcbiAgICAgIG11bHRpcGFydDogeyBsaW1pdHM6IHsgZmlsZVNpemU6IDEwMjQgKiAxMDI0ICogMzAgfSB9LFxuICAgICAgc3RhdGljOiB7XG4gICAgICAgIHJvb3Q6IHBhdGguam9pbihpbXBvcnQubWV0YS5kaXJuYW1lLCBcIi8uLi9cIiwgXCJwdWJsaWNcIiksXG4gICAgICAgIHByZWZpeDogXCIvYXBpL3B1YmxpY1wiLFxuICAgICAgfSxcbiAgICAgIGN1c3RvbTogKHNlcnZlcikgPT4ge1xuICAgICAgICAvLyBPQXV0aCDsvZzrsLEg4oCUIEFudGhyb3BpY+ydtCAvY2FsbGJhY2vsnLzroZwg66as64uk7J2066CJ7Yq4XG4gICAgICAgIHNlcnZlci5nZXQoXCIvY2FsbGJhY2tcIiwgYXN5bmMgKHJlcXVlc3QsIHJlcGx5KSA9PiB7XG4gICAgICAgICAgY29uc3QgeyBjb2RlLCBzdGF0ZSB9ID0gcmVxdWVzdC5xdWVyeSBhcyB7IGNvZGU/OiBzdHJpbmc7IHN0YXRlPzogc3RyaW5nIH07XG4gICAgICAgICAgaWYgKCFjb2RlIHx8ICFzdGF0ZSkge1xuICAgICAgICAgICAgcmV0dXJuIHJlcGx5LnJlZGlyZWN0KFwiLz9vYXV0aD1lcnJvciZyZWFzb249bWlzc2luZ19wYXJhbXNcIik7XG4gICAgICAgICAgfVxuICAgICAgICAgIHRyeSB7XG4gICAgICAgICAgICByZXR1cm4gUWdyaWRGcmFtZS5oYW5kbGVPQXV0aENhbGxiYWNrKGNvZGUsIHN0YXRlLCByZXBseSk7XG4gICAgICAgICAgfSBjYXRjaCAoZSkge1xuICAgICAgICAgICAgcmV0dXJuIHJlcGx5LnJlZGlyZWN0KFxuICAgICAgICAgICAgICBgLz9vYXV0aD1lcnJvciZyZWFzb249JHtlbmNvZGVVUklDb21wb25lbnQoKGUgYXMgRXJyb3IpLm1lc3NhZ2UpfWAsXG4gICAgICAgICAgICApO1xuICAgICAgICAgIH1cbiAgICAgICAgfSk7XG5cbiAgICAgICAgLy8gT3BlbkFJIE9BdXRoIOuKlCBjb2RleCBhcHAtc2VydmVyIOqwgCDsnpDssrQgY2FsbGJhY2sg7ISc67KE66W8IOyYrOumvFxuICAgICAgICAvLyBoYW5kbGVPcGVuQUlDYWxsYmFjayDrtojtlYTsmpQg4oCUIG9hdXRoQ29tcGxldGVPcGVuQUkgQVBJIOuhnCDsmYTro4wg7ZmV7J24XG4gICAgICB9LFxuICAgIH0sXG4gICAgYXBpQ29uZmlnOiB7XG4gICAgICBjb250ZXh0UHJvdmlkZXI6IChkZWZhdWx0Q29udGV4dCwgcmVxdWVzdCkgPT4ge1xuICAgICAgICByZXR1cm4ge1xuICAgICAgICAgIC4uLmRlZmF1bHRDb250ZXh0LFxuICAgICAgICAgIGlwOiByZXF1ZXN0LmlwLFxuICAgICAgICAgIGJvZHk6IHJlcXVlc3QuYm9keSxcbiAgICAgICAgfTtcbiAgICAgIH0sXG4gICAgICBndWFyZEhhbmRsZXI6ICgpID0+IHt9LFxuICAgICAgY2FjaGVDb250cm9sSGFuZGxlcjogKHJlcSkgPT4ge1xuICAgICAgICBzd2l0Y2ggKHJlcS50eXBlKSB7XG4gICAgICAgICAgY2FzZSBcImFzc2V0c1wiOlxuICAgICAgICAgICAgLy8gSGFzaCDtj6ztlajrkJwg7YyM7J28OiDsmIHqtawg7LqQ7IucXG4gICAgICAgICAgICBpZiAocmVxLnBhdGgubWF0Y2goLy1bYS1mMC05XStcXC4vKSkge1xuICAgICAgICAgICAgICByZXR1cm4gQ2FjaGVQcmVzZXRzLmltbXV0YWJsZTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIHJldHVybiBDYWNoZVByZXNldHMubG9uZ0xpdmVkO1xuXG4gICAgICAgICAgY2FzZSBcImFwaVwiOlxuICAgICAgICAgICAgLy8gR0VUIOyalOyyreunjCDsupDsi7Eg6rOg66CkXG4gICAgICAgICAgICBpZiAocmVxLm1ldGhvZCA9PT0gXCJHRVRcIikge1xuICAgICAgICAgICAgICAvLyDtirnsoJUg6rK966Gc64qUIOynp+ydgCDsupDsi5xcbiAgICAgICAgICAgICAgaWYgKHJlcS5wYXRoLnN0YXJ0c1dpdGgoXCIvYXBpL3N0YXRpYy1kYXRhXCIpKSB7XG4gICAgICAgICAgICAgICAgcmV0dXJuIENhY2hlUHJlc2V0cy5zaG9ydExpdmVkO1xuICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgIGlmIChyZXEucGF0aC5zdGFydHNXaXRoKFwiL2FwaS90ZXJtc1wiKSkge1xuICAgICAgICAgICAgICAgIHJldHVybiBDYWNoZVByZXNldHMubWVkaXVtTGl2ZWQ7XG4gICAgICAgICAgICAgIH1cbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIC8vIOq4sOuzuDog7LqQ7IucIOyXhuydjFxuICAgICAgICAgICAgcmV0dXJuIENhY2hlUHJlc2V0cy5ub0NhY2hlO1xuXG4gICAgICAgICAgY2FzZSBcInNzclwiOlxuICAgICAgICAgICAgLy8gU1NSIO2OmOydtOyngDogMTDstIgg7LqQ7IucXG4gICAgICAgICAgICByZXR1cm4gQ2FjaGVQcmVzZXRzLnNzcjtcblxuICAgICAgICAgIGNhc2UgXCJjc3JcIjpcbiAgICAgICAgICAgIC8vIENTUiBmYWxsYmFjayAoaW5kZXguaHRtbCk6IDHrtoQg7LqQ7IucXG4gICAgICAgICAgICByZXR1cm4gQ2FjaGVQcmVzZXRzLnNob3J0TGl2ZWQ7XG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIENhY2hlUHJlc2V0cy5ub0NhY2hlO1xuICAgICAgfSxcbiAgICB9LFxuICAgIGNhY2hlOiB7XG4gICAgICBkZWZhdWx0OiBcIm1haW5cIixcbiAgICAgIHN0b3Jlczoge1xuICAgICAgICBtYWluOiBzdG9yZSgpLnVzZUwxTGF5ZXIoY2FjaGVEcml2ZXJzLm1lbW9yeSh7IG1heFNpemU6IFwiNTBtYlwiIH0pKSxcbiAgICAgIH0sXG4gICAgICB0dGw6IFwiNW1cIixcbiAgICAgIHByZWZpeDogXCJcIixcbiAgICB9LFxuICAgIGxpZmVjeWNsZToge1xuICAgICAgb25TdGFydDogYXN5bmMgKCkgPT4ge1xuICAgICAgICBjb25zdCBsb2cgPSBnZXRMb2dnZXIoW1wicWdyaWRcIiwgXCJzdGFydHVwXCJdKTtcblxuICAgICAgICBsZXQgdHJpZ2dlclJlYWR5ID0gdHJ1ZTtcbiAgICAgICAgdHJ5IHtcbiAgICAgICAgICBhd2FpdCBlbnN1cmVUb2tlbnNUcmlnZ2VyKGNvbm5Db25maWcpO1xuICAgICAgICB9IGNhdGNoIChlKSB7XG4gICAgICAgICAgdHJpZ2dlclJlYWR5ID0gZmFsc2U7XG4gICAgICAgICAgbG9nLndhcm4oYHRyaWdnZXIgc2V0dXAgZmFpbGVkOiAkeyhlIGFzIEVycm9yKS5tZXNzYWdlfWApO1xuICAgICAgICB9XG5cbiAgICAgICAgY29uc3Qgc3Vic2NyaWJlciA9IG5ldyBUb2tlblN1YnNjcmliZXIoY29ubkNvbmZpZywgUWdyaWREaXNwYXRjaGVyKTtcbiAgICAgICAgUWdyaWREaXNwYXRjaGVyLnN1YnNjcmliZXIgPSBzdWJzY3JpYmVyO1xuXG4gICAgICAgIGNvbnN0IHN0YXJ0ZWQgPSBhd2FpdCBzdWJzY3JpYmVyLnN0YXJ0KCk7XG5cbiAgICAgICAgLy8g6riw64+ZIOyDge2DnOulvCDrgqjquLTri6Qg4oCUIEhUVFAg64qUIOydtOuvuCDsl7TroKQg7J6I7Ja07IScLCDspIDruYQg7KCEIOyalOyyreydtCBcIuyeoOyLnCDtm4Qg65CY64qU7KeAXCJcbiAgICAgICAgLy8gXCLsnqzsi5zrj4TtlbTrj4Qg7JWIIOuQmOuKlOyngFwi66W8IOq1rOu2hO2VtCDsnZHri7XtlbTslbwg7ZWc64ukKDUwMyB2cyA1MDApLlxuICAgICAgICB0cnkge1xuICAgICAgICAgIGNvbnN0IG9wZW5haURpc3BhdGNoZXIgPSBuZXcgT3BlbkFJRGlzcGF0Y2hlcigpO1xuICAgICAgICAgIGF3YWl0IG9wZW5haURpc3BhdGNoZXIuc3RhcnQoKTtcbiAgICAgICAgICBRZ3JpZERpc3BhdGNoZXIub3BlbmFpRGlzcGF0Y2hlciA9IG9wZW5haURpc3BhdGNoZXI7XG4gICAgICAgICAgUWdyaWREaXNwYXRjaGVyLnN0YXJ0dXBTdGF0ZS5vcGVuYWkgPSBcInJlYWR5XCI7XG4gICAgICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgICBRZ3JpZERpc3BhdGNoZXIuc3RhcnR1cFN0YXRlLm9wZW5haSA9IFwiZmFpbGVkXCI7XG4gICAgICAgICAgbG9nLndhcm4oYG9wZW5haSBkaXNwYXRjaGVyIGZhaWxlZDogJHsoZSBhcyBFcnJvcikubWVzc2FnZX1gKTtcbiAgICAgICAgfVxuXG4gICAgICAgIHRyeSB7XG4gICAgICAgICAgY29uc3QgYW50aHJvcGljRGlzcGF0Y2hlciA9IG5ldyBBbnRocm9waWNEaXNwYXRjaGVyKCk7XG4gICAgICAgICAgYXdhaXQgYW50aHJvcGljRGlzcGF0Y2hlci5zdGFydCgpO1xuICAgICAgICAgIFFncmlkRGlzcGF0Y2hlci5hbnRocm9waWNEaXNwYXRjaGVyID0gYW50aHJvcGljRGlzcGF0Y2hlcjtcbiAgICAgICAgICBRZ3JpZERpc3BhdGNoZXIuc3RhcnR1cFN0YXRlLmFudGhyb3BpYyA9IFwicmVhZHlcIjtcbiAgICAgICAgfSBjYXRjaCAoZSkge1xuICAgICAgICAgIFFncmlkRGlzcGF0Y2hlci5zdGFydHVwU3RhdGUuYW50aHJvcGljID0gXCJmYWlsZWRcIjtcbiAgICAgICAgICBsb2cud2FybihgYW50aHJvcGljIGRpc3BhdGNoZXIgZmFpbGVkOiAkeyhlIGFzIEVycm9yKS5tZXNzYWdlfWApO1xuICAgICAgICB9XG5cbiAgICAgICAgc3RhcnRFeHBpcmVkVG9rZW5EaWdlc3QoKTtcblxuICAgICAgICBjb25zdCBhbnRocm9waWNDb3VudCA9IFFncmlkRGlzcGF0Y2hlci5hbnRocm9waWNEaXNwYXRjaGVyPy50b2tlbkNvdW50ID8/IDA7XG4gICAgICAgIGNvbnN0IG9wZW5haVJlYWR5ID0gUWdyaWREaXNwYXRjaGVyLm9wZW5haURpc3BhdGNoZXI/LnJlYWR5V29ya2VyQ291bnQgPz8gMDtcbiAgICAgICAgY29uc3Qgb3BlbmFpVG90YWwgPSBRZ3JpZERpc3BhdGNoZXIub3BlbmFpRGlzcGF0Y2hlcj8ud29ya2VyQ291bnQgPz8gMDtcblxuICAgICAgICBsb2cuaW5mbyhgbGlzdGVuaW5nIG9uIGh0dHA6Ly8ke2hvc3R9OiR7cG9ydH1gKTtcbiAgICAgICAgbG9nLmluZm8oYGFudGhyb3BpYzogJHthbnRocm9waWNDb3VudH0gdG9rZW5zIHJlYWR5YCk7XG4gICAgICAgIGxvZy5pbmZvKGBvcGVuYWk6ICR7b3BlbmFpUmVhZHl9LyR7b3BlbmFpVG90YWx9IHRva2VucyByZWFkeWApO1xuICAgICAgICBsb2cuaW5mbyhcbiAgICAgICAgICBgc3Vic2NyaWJlcjogJHtzdGFydGVkID8gXCJMSVNURU4gYWN0aXZlXCIgOiBcImRlZ3JhZGVkXCJ9JHt0cmlnZ2VyUmVhZHkgPyBcIlwiIDogXCIsIHRyaWdnZXIgZmFpbGVkXCJ9YCxcbiAgICAgICAgKTtcbiAgICAgIH0sXG4gICAgICBvblNodXRkb3duOiBhc3luYyAoKSA9PiB7XG4gICAgICAgIGNvbnN0IGxvZyA9IGdldExvZ2dlcihbXCJxZ3JpZFwiLCBcInN0YXJ0dXBcIl0pO1xuICAgICAgICBzdG9wRXhwaXJlZFRva2VuRGlnZXN0KCk7XG4gICAgICAgIGlmIChRZ3JpZERpc3BhdGNoZXIub3BlbmFpRGlzcGF0Y2hlcikge1xuICAgICAgICAgIGF3YWl0IFFncmlkRGlzcGF0Y2hlci5vcGVuYWlEaXNwYXRjaGVyLnN0b3AoKTtcbiAgICAgICAgfVxuICAgICAgICBpZiAoUWdyaWREaXNwYXRjaGVyLmFudGhyb3BpY0Rpc3BhdGNoZXIpIHtcbiAgICAgICAgICBhd2FpdCBRZ3JpZERpc3BhdGNoZXIuYW50aHJvcGljRGlzcGF0Y2hlci5zdG9wKCk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKFFncmlkRGlzcGF0Y2hlci5zdWJzY3JpYmVyKSB7XG4gICAgICAgICAgYXdhaXQgUWdyaWREaXNwYXRjaGVyLnN1YnNjcmliZXIuc3RvcCgpO1xuICAgICAgICB9XG4gICAgICAgIGxvZy5pbmZvKFwiZ3JhY2VmdWwgc2h1dGRvd25cIik7XG4gICAgICB9LFxuICAgICAgb25FcnJvcjogKGVycm9yLCBfcmVxdWVzdCwgcmVwbHkpID0+IHtcbiAgICAgICAgaGFuZGxlU2VydmVyRXJyb3IoZXJyb3IsIHJlcGx5KTtcbiAgICAgIH0sXG4gICAgfSxcbiAgfSxcbn0pO1xuIl0sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7QUFxQkEsTUFBTSxPQUFPLFFBQVEsSUFBSSxRQUFRO0FBQ2pDLE1BQU0sT0FBTyxPQUFPLFFBQVEsSUFBSSxRQUFRLEtBQUs7QUFFN0MsTUFBTSxhQUFhO0NBQ2pCLE1BQU0sUUFBUSxJQUFJLGtCQUFrQjtDQUNwQyxNQUFNLE9BQU8sUUFBUSxJQUFJLGtCQUFrQixJQUFJO0NBQy9DLE1BQU0sUUFBUSxJQUFJLGtCQUFrQjtDQUNwQyxVQUFVLFFBQVEsSUFBSSxzQkFBc0I7Q0FDNUMsVUFBVSxRQUFRLElBQUksa0JBQWtCO0FBQzFDO0FBRUEsSUFBQSx3QkFBZSxhQUFhO0NBQzFCLGFBQWEsUUFBUSxJQUFJLGdCQUFnQjtDQUN6QyxVQUFVLEVBQ1IsZ0JBQWdCLEVBQ2QsWUFBWTtFQUNWLEdBQUc7RUFDSCxXQUFXO0VBQ1gsNkJBQTZCO0NBQy9CLEVBQ0YsRUFDRjtDQUNBLEtBQUs7RUFD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
|
|
@@ -45,8 +45,8 @@ async function notifySlack(notification) {
|
|
|
45
45
|
},
|
|
46
46
|
body: JSON.stringify({
|
|
47
47
|
channel,
|
|
48
|
-
text: subject ? `${title} — ${subject}` : title,
|
|
49
48
|
attachments: [{
|
|
49
|
+
fallback: subject ? `${title} — ${subject}` : title,
|
|
50
50
|
...color ? { color } : {},
|
|
51
51
|
blocks
|
|
52
52
|
}]
|
|
@@ -62,4 +62,4 @@ async function notifySlack(notification) {
|
|
|
62
62
|
//#endregion
|
|
63
63
|
export { SLACK_COLOR, notifySlack };
|
|
64
64
|
|
|
65
|
-
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2xhY2stbm90aWZ5LmpzIiwibmFtZXMiOltdLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy91dGlscy9zbGFjay1ub3RpZnkudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBTbGFjayDslYzrprwgKGNoYXQucG9zdE1lc3NhZ2UpLlxuICpcbiAqIGZhaWwtb3BlbjogZW52IOuvuOyEpOygleydtOuptCDsobDsmqntnoggbm8tb3Ag7J206rOgLCDsoITshqEg7Iuk7Yyo64qUIHdhcm4g66Gc6re466Gc66eMIOuCqOq4tOuLpC5cbiAqIOyVjOumvCDqsr3roZzqsIAg7Yag7YGwIOudvOyasO2MheydtOuCmCDsmKTrpZgg7KCE7YyM66W8IOyngOyXsMK37LCo64uo7ZW07ISc64qUIOyViCDrkJzri6QuXG4gKi9cbmltcG9ydCB7IGdldExvZ2dlciB9IGZyb20gXCJAbG9ndGFwZS9sb2d0YXBlXCI7XG5cbmNvbnN0IGxvZ2dlciA9IGdldExvZ2dlcihbXCJxZ3JpZFwiLCBcInNsYWNrXCJdKTtcblxuY29uc3QgU0xBQ0tfUE9TVF9NRVNTQUdFX1VSTCA9IFwiaHR0cHM6Ly9zbGFjay5jb20vYXBpL2NoYXQucG9zdE1lc3NhZ2VcIjtcbmNvbnN0IFNMQUNLX1RJTUVPVVRfTVMgPSA1XzAwMDtcblxuLyoqIOyDge2DnOulvCDsg4nsnLzroZwg66i87KCAIOydve2eiOqyjCDtlZjripQgYXR0YWNobWVudCDsg4nsg4Eg67CULiAqL1xuZXhwb3J0IGNvbnN0IFNMQUNLX0NPTE9SID0ge1xuICBnb29kOiBcIiMyZWI4ODZcIixcbiAgYmFkOiBcIiNlMDFlNWFcIixcbn0gYXMgY29uc3Q7XG5cbmV4cG9ydCB0eXBlIFNsYWNrTm90aWZpY2F0aW9uID0ge1xuICAvKiog7ZWcIOykhCDsoJzrqqkuIOustOyKqCDsnbzsnbQg7J287Ja064Ks64qU7KeA66eMIOuLtOuKlOuLpC4gKi9cbiAgdGl0bGU6IHN0cmluZztcbiAgLyoqIOygnOuqqSDsmIYg7L2U65OcIOyKpO2DgOydvOuhnCDrtpnripQg64yA7IOBIOyLneuzhOyekC4gKi9cbiAgc3ViamVjdD86IHN0cmluZztcbiAgLyoqIOygnOuqqSDslYTrnpgg7J6R7J2AIOq4gOyUqOuhnCDrgrTroKTqsIDripQg67aA6rCAIOygleuztCDigJQg7J297KeAIOyViuyVhOuPhCDrkJjripQg6rKD65OkLiAqL1xuICBjb250ZXh0Pzogc3RyaW5nO1xuICBjb2xvcj86ICh0eXBlb2YgU0xBQ0tfQ09MT1IpW2tleW9mIHR5cGVvZiBTTEFDS19DT0xPUl07XG59O1xuXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gbm90aWZ5U2xhY2sobm90aWZpY2F0aW9uOiBTbGFja05vdGlmaWNhdGlvbik6IFByb21pc2U8dm9pZD4ge1xuICBjb25zdCBib3RUb2tlbiA9IHByb2Nlc3MuZW52LlNMQUNLX0JPVF9UT0tFTjtcbiAgY29uc3QgY2hhbm5lbCA9IHByb2Nlc3MuZW52LlNMQUNLX0NIQU5ORUxfSUQ7XG4gIGNvbnN0IHsgdGl0bGUsIHN1YmplY3QsIGNvbnRleHQsIGNvbG9yIH0gPSBub3RpZmljYXRpb247XG4gIGlmICghYm90VG9rZW4gfHwgIWNoYW5uZWwpIHtcbiAgICBsb2dnZXIuZGVidWcoYHNsYWNrIG5vdCBjb25maWd1cmVkLCBza2lwcGluZyBub3RpZmljYXRpb246ICR7dGl0bGV9ICR7c3ViamVjdCA/
|
|
65
|
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2xhY2stbm90aWZ5LmpzIiwibmFtZXMiOltdLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy91dGlscy9zbGFjay1ub3RpZnkudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBTbGFjayDslYzrprwgKGNoYXQucG9zdE1lc3NhZ2UpLlxuICpcbiAqIGZhaWwtb3BlbjogZW52IOuvuOyEpOygleydtOuptCDsobDsmqntnoggbm8tb3Ag7J206rOgLCDsoITshqEg7Iuk7Yyo64qUIHdhcm4g66Gc6re466Gc66eMIOuCqOq4tOuLpC5cbiAqIOyVjOumvCDqsr3roZzqsIAg7Yag7YGwIOudvOyasO2MheydtOuCmCDsmKTrpZgg7KCE7YyM66W8IOyngOyXsMK37LCo64uo7ZW07ISc64qUIOyViCDrkJzri6QuXG4gKi9cbmltcG9ydCB7IGdldExvZ2dlciB9IGZyb20gXCJAbG9ndGFwZS9sb2d0YXBlXCI7XG5cbmNvbnN0IGxvZ2dlciA9IGdldExvZ2dlcihbXCJxZ3JpZFwiLCBcInNsYWNrXCJdKTtcblxuY29uc3QgU0xBQ0tfUE9TVF9NRVNTQUdFX1VSTCA9IFwiaHR0cHM6Ly9zbGFjay5jb20vYXBpL2NoYXQucG9zdE1lc3NhZ2VcIjtcbmNvbnN0IFNMQUNLX1RJTUVPVVRfTVMgPSA1XzAwMDtcblxuLyoqIOyDge2DnOulvCDsg4nsnLzroZwg66i87KCAIOydve2eiOqyjCDtlZjripQgYXR0YWNobWVudCDsg4nsg4Eg67CULiAqL1xuZXhwb3J0IGNvbnN0IFNMQUNLX0NPTE9SID0ge1xuICBnb29kOiBcIiMyZWI4ODZcIixcbiAgYmFkOiBcIiNlMDFlNWFcIixcbn0gYXMgY29uc3Q7XG5cbmV4cG9ydCB0eXBlIFNsYWNrTm90aWZpY2F0aW9uID0ge1xuICAvKiog7ZWcIOykhCDsoJzrqqkuIOustOyKqCDsnbzsnbQg7J287Ja064Ks64qU7KeA66eMIOuLtOuKlOuLpC4gKi9cbiAgdGl0bGU6IHN0cmluZztcbiAgLyoqIOygnOuqqSDsmIYg7L2U65OcIOyKpO2DgOydvOuhnCDrtpnripQg64yA7IOBIOyLneuzhOyekC4gKi9cbiAgc3ViamVjdD86IHN0cmluZztcbiAgLyoqIOygnOuqqSDslYTrnpgg7J6R7J2AIOq4gOyUqOuhnCDrgrTroKTqsIDripQg67aA6rCAIOygleuztCDigJQg7J297KeAIOyViuyVhOuPhCDrkJjripQg6rKD65OkLiAqL1xuICBjb250ZXh0Pzogc3RyaW5nO1xuICBjb2xvcj86ICh0eXBlb2YgU0xBQ0tfQ09MT1IpW2tleW9mIHR5cGVvZiBTTEFDS19DT0xPUl07XG59O1xuXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gbm90aWZ5U2xhY2sobm90aWZpY2F0aW9uOiBTbGFja05vdGlmaWNhdGlvbik6IFByb21pc2U8dm9pZD4ge1xuICBjb25zdCBib3RUb2tlbiA9IHByb2Nlc3MuZW52LlNMQUNLX0JPVF9UT0tFTjtcbiAgY29uc3QgY2hhbm5lbCA9IHByb2Nlc3MuZW52LlNMQUNLX0NIQU5ORUxfSUQ7XG4gIGNvbnN0IHsgdGl0bGUsIHN1YmplY3QsIGNvbnRleHQsIGNvbG9yIH0gPSBub3RpZmljYXRpb247XG4gIGlmICghYm90VG9rZW4gfHwgIWNoYW5uZWwpIHtcbiAgICBsb2dnZXIuZGVidWcoYHNsYWNrIG5vdCBjb25maWd1cmVkLCBza2lwcGluZyBub3RpZmljYXRpb246ICR7dGl0bGV9ICR7c3ViamVjdCA/PyBcIlwifWApO1xuICAgIHJldHVybjtcbiAgfVxuXG4gIGNvbnN0IGJsb2NrczogdW5rbm93bltdID0gW1xuICAgIHtcbiAgICAgIHR5cGU6IFwic2VjdGlvblwiLFxuICAgICAgdGV4dDogeyB0eXBlOiBcIm1ya2R3blwiLCB0ZXh0OiBzdWJqZWN0ID8gYCoke3RpdGxlfSogIFxcYCR7c3ViamVjdH1cXGBgIDogYCoke3RpdGxlfSpgIH0sXG4gICAgfSxcbiAgXTtcbiAgaWYgKGNvbnRleHQpIHtcbiAgICBibG9ja3MucHVzaCh7IHR5cGU6IFwiY29udGV4dFwiLCBlbGVtZW50czogW3sgdHlwZTogXCJtcmtkd25cIiwgdGV4dDogY29udGV4dCB9XSB9KTtcbiAgfVxuXG4gIHRyeSB7XG4gICAgY29uc3QgcmVzID0gYXdhaXQgZmV0Y2goU0xBQ0tfUE9TVF9NRVNTQUdFX1VSTCwge1xuICAgICAgbWV0aG9kOiBcIlBPU1RcIixcbiAgICAgIGhlYWRlcnM6IHtcbiAgICAgICAgXCJDb250ZW50LVR5cGVcIjogXCJhcHBsaWNhdGlvbi9qc29uOyBjaGFyc2V0PXV0Zi04XCIsXG4gICAgICAgIEF1dGhvcml6YXRpb246IGBCZWFyZXIgJHtib3RUb2tlbn1gLFxuICAgICAgfSxcbiAgICAgIGJvZHk6IEpTT04uc3RyaW5naWZ5KHtcbiAgICAgICAgY2hhbm5lbCxcbiAgICAgICAgLy8gZmFsbGJhY2sg7J2AIGF0dGFjaG1lbnQg7JWI7JeQIOuRlOuLpC4g7LWc7IOB7JyEIHRleHQg66GcIOuztOuCtOuptCBibG9ja3Mg6rCAIOygleyDgSDroIzrjZTrkJjripRcbiAgICAgICAgLy8g7ZmU66m07JeQ7ISc64+EIO2VqOq7mCDstpzroKXrj7wg7KCc66qp7J20IOuRkCDrsogg67O07J2464ukLiBhdHRhY2htZW50IGZhbGxiYWNrIOydgCBibG9ja3Mg66W8XG4gICAgICAgIC8vIOuquyDqt7jrpqzripQg7YG065287J207Ja47Yq47JmAIO2RuOyLnCDslYzrprzsl5DshJzrp4wg7JOw7J2464ukLlxuICAgICAgICBhdHRhY2htZW50czogW1xuICAgICAgICAgIHtcbiAgICAgICAgICAgIGZhbGxiYWNrOiBzdWJqZWN0ID8gYCR7dGl0bGV9IOKAlCAke3N1YmplY3R9YCA6IHRpdGxlLFxuICAgICAgICAgICAgLi4uKGNvbG9yID8geyBjb2xvciB9IDoge30pLFxuICAgICAgICAgICAgYmxvY2tzLFxuICAgICAgICAgIH0sXG4gICAgICAgIF0sXG4gICAgICB9KSxcbiAgICAgIHNpZ25hbDogQWJvcnRTaWduYWwudGltZW91dChTTEFDS19USU1FT1VUX01TKSxcbiAgICB9KTtcblxuICAgIC8vIGJvdCDrr7jstIjrjIAobm90X2luX2NoYW5uZWwpwrfsnpjrqrvrkJwg7LGE64SQIOuTsSDqsIDsnqUg7Z2U7ZWcIOyYpOyEpOygleydgCBIVFRQIDIwMCArIG9rOmZhbHNlIOuhnCDsmKjri6QuXG4gICAgLy8gc3RhdHVzIOunjCDrs7TrqbQg7KGw7Jqp7Z6IIOyEseqztSDsspjrpqzrj7wg7JWM66a87J20IO2GteynuOuhnCDsgqzrnbzsp4Tri6QuXG4gICAgY29uc3QgYm9keSA9IChhd2FpdCByZXMuanNvbigpLmNhdGNoKCgpID0+IG51bGwpKSBhcyB7IG9rPzogYm9vbGVhbjsgZXJyb3I/OiBzdHJpbmcgfSB8IG51bGw7XG4gICAgaWYgKCFib2R5Py5vaykge1xuICAgICAgbG9nZ2VyLndhcm4oYHNsYWNrIG5vdGlmaWNhdGlvbiByZWplY3RlZDogJHtib2R5Py5lcnJvciA/PyBgaHR0cCAke3Jlcy5zdGF0dXN9YH1gKTtcbiAgICB9XG4gIH0gY2F0Y2ggKGUpIHtcbiAgICBsb2dnZXIud2Fybihgc2xhY2sgbm90aWZpY2F0aW9uIGZhaWxlZDogJHsoZSBhcyBFcnJvcikubWVzc2FnZX1gKTtcbiAgfVxufVxuIl0sIm1hcHBpbmdzIjoiOzs7Ozs7OztBQVFBLE1BQU0sU0FBUyxVQUFVLENBQUMsU0FBUyxPQUFPLENBQUM7QUFFM0MsTUFBTSx5QkFBeUI7QUFDL0IsTUFBTSxtQkFBbUI7O0FBR3pCLE1BQWEsY0FBYztDQUN6QixNQUFNO0NBQ04sS0FBSztBQUNQO0FBWUEsZUFBc0IsWUFBWSxjQUFnRDtDQUNoRixNQUFNLFdBQVcsUUFBUSxJQUFJO0NBQzdCLE1BQU0sVUFBVSxRQUFRLElBQUk7Q0FDNUIsTUFBTSxFQUFFLE9BQU8sU0FBUyxTQUFTLFVBQVU7Q0FDM0MsSUFBSSxDQUFDLFlBQVksQ0FBQyxTQUFTO0VBQ3pCLE9BQU8sTUFBTSxnREFBZ0QsTUFBTSxHQUFHLFdBQVcsSUFBSTtFQUNyRjtDQUNGO0NBRUEsTUFBTSxTQUFvQixDQUN4QjtFQUNFLE1BQU07RUFDTixNQUFNO0dBQUUsTUFBTTtHQUFVLE1BQU0sVUFBVSxJQUFJLE1BQU0sT0FBTyxRQUFRLE1BQU0sSUFBSSxNQUFNO0VBQUc7Q0FDdEYsQ0FDRjtDQUNBLElBQUksU0FDRixPQUFPLEtBQUs7RUFBRSxNQUFNO0VBQVcsVUFBVSxDQUFDO0dBQUUsTUFBTTtHQUFVLE1BQU07RUFBUSxDQUFDO0NBQUUsQ0FBQztDQUdoRixJQUFJO0VBQ0YsTUFBTSxNQUFNLE1BQU0sTUFBTSx3QkFBd0I7R0FDOUMsUUFBUTtHQUNSLFNBQVM7SUFDUCxnQkFBZ0I7SUFDaEIsZUFBZSxVQUFVO0dBQzNCO0dBQ0EsTUFBTSxLQUFLLFVBQVU7SUFDbkI7SUFJQSxhQUFhLENBQ1g7S0FDRSxVQUFVLFVBQVUsR0FBRyxNQUFNLEtBQUssWUFBWTtLQUM5QyxHQUFJLFFBQVEsRUFBRSxNQUFNLElBQUksQ0FBQztLQUN6QjtJQUNGLENBQ0Y7R0FDRixDQUFDO0dBQ0QsUUFBUSxZQUFZLFFBQVEsZ0JBQWdCO0VBQzlDLENBQUM7RUFJRCxNQUFNLE9BQVEsTUFBTSxJQUFJLEtBQUssQ0FBQyxDQUFDLFlBQVksSUFBSTtFQUMvQyxJQUFJLENBQUMsTUFBTSxJQUNULE9BQU8sS0FBSyxnQ0FBZ0MsTUFBTSxTQUFTLFFBQVEsSUFBSSxVQUFVO0NBRXJGLFNBQVMsR0FBRztFQUNWLE9BQU8sS0FBSyw4QkFBK0IsRUFBWSxTQUFTO0NBQ2xFO0FBQ0YifQ==
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { buildDigestContext } from "./expired-token-digest";
|
|
4
|
+
|
|
5
|
+
const userMap = new Map([
|
|
6
|
+
["yds", "U09NAJUQSFQ"],
|
|
7
|
+
["haze", "U09N96NHZB7"],
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
describe("buildDigestContext", () => {
|
|
11
|
+
it("매핑된 토큰은 멘션으로 개인에게 꽂는다", () => {
|
|
12
|
+
const text = buildDigestContext([{ name: "anthropic/yds", provider: "anthropic" }], userMap);
|
|
13
|
+
|
|
14
|
+
expect(text).toBe("<@U09NAJUQSFQ> anthropic");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("공용 계정처럼 매핑이 없으면 멘션 없이 이름만 남긴다", () => {
|
|
18
|
+
const text = buildDigestContext(
|
|
19
|
+
[{ name: "anthropic/dev-common", provider: "anthropic" }],
|
|
20
|
+
userMap,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
expect(text).toBe("anthropic/dev-common (anthropic)");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("같은 사람의 provider 별 토큰을 각각 줄로 나열한다", () => {
|
|
27
|
+
const text = buildDigestContext(
|
|
28
|
+
[
|
|
29
|
+
{ name: "anthropic/yds", provider: "anthropic" },
|
|
30
|
+
{ name: "openai/yds", provider: "openai" },
|
|
31
|
+
],
|
|
32
|
+
userMap,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
expect(text.split("\n")).toEqual(["<@U09NAJUQSFQ> anthropic", "<@U09NAJUQSFQ> openai"]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("provider prefix 없이 저장된 이름도 매핑한다", () => {
|
|
39
|
+
const text = buildDigestContext([{ name: "haze", provider: "anthropic" }], userMap);
|
|
40
|
+
|
|
41
|
+
expect(text).toBe("<@U09N96NHZB7> anthropic");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("이름이 없는 토큰도 줄을 잃지 않는다", () => {
|
|
45
|
+
const text = buildDigestContext([{ name: null, provider: "openai" }], userMap);
|
|
46
|
+
|
|
47
|
+
expect(text).toBe("unnamed (openai)");
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 비활성 토큰 주기 알림.
|
|
3
|
+
*
|
|
4
|
+
* 사망 순간의 단발 알림(`token-death.ts`)은 그 시점에 채널을 보지 않으면 놓친다. 재로그인은
|
|
5
|
+
* 사람이 해야 하는 일이라 놓치면 토큰이 계속 빠진 채로 남는다. 그래서 남아 있는 비활성
|
|
6
|
+
* 토큰을 주기적으로 다시 알리고, 소유자를 멘션해 개인에게 꽂는다.
|
|
7
|
+
*
|
|
8
|
+
* 비활성 토큰이 없으면 보내지 않는다 — "이상 없음"을 주기적으로 알리면 채널이 무뎌진다.
|
|
9
|
+
*/
|
|
10
|
+
import { getLogger } from "@logtape/logtape";
|
|
11
|
+
|
|
12
|
+
import { notifySlack, SLACK_COLOR } from "../../utils/slack-notify";
|
|
13
|
+
import { TokenModel } from "../token/token.model";
|
|
14
|
+
|
|
15
|
+
const logger = getLogger(["qgrid", "expired-digest"]);
|
|
16
|
+
|
|
17
|
+
const MINUTE_MS = 60_000;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* `토큰명:SlackUserId` 목록. 공용 계정(bysuco, dev-common 등)은 넣지 않는다 — 매핑이 없으면
|
|
21
|
+
* 멘션 없이 이름만 나가고, 그게 맞는 동작이다.
|
|
22
|
+
*
|
|
23
|
+
* Slack 표시 이름과 토큰명이 자주 어긋나(haze→haze.lee, byeongjun→potados) 자동 매칭은
|
|
24
|
+
* 조용히 실패한다. 멘션이 빠지면 알림의 목적 자체가 사라지므로 명시 매핑만 쓴다.
|
|
25
|
+
*/
|
|
26
|
+
function parseUserMap(raw: string | undefined): Map<string, string> {
|
|
27
|
+
const map = new Map<string, string>();
|
|
28
|
+
if (!raw) return map;
|
|
29
|
+
|
|
30
|
+
for (const entry of raw.split(",")) {
|
|
31
|
+
const [name, userId] = entry.split(":").map((s) => s.trim());
|
|
32
|
+
if (name && userId) map.set(name, userId);
|
|
33
|
+
}
|
|
34
|
+
return map;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** `anthropic/yds` → `yds`. provider prefix 없이 저장된 이름도 그대로 받는다. */
|
|
38
|
+
function ownerOf(tokenName: string | null): string {
|
|
39
|
+
if (!tokenName) return "";
|
|
40
|
+
const slash = tokenName.indexOf("/");
|
|
41
|
+
return slash === -1 ? tokenName : tokenName.slice(slash + 1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function buildDigestContext(
|
|
45
|
+
tokens: { name: string | null; provider: string }[],
|
|
46
|
+
userMap: Map<string, string>,
|
|
47
|
+
): string {
|
|
48
|
+
return tokens
|
|
49
|
+
.map((token) => {
|
|
50
|
+
const userId = userMap.get(ownerOf(token.name));
|
|
51
|
+
const label = token.name ?? "unnamed";
|
|
52
|
+
// 멘션이 있으면 이름 대신 멘션을 쓴다 — 둘 다 쓰면 같은 말이 두 번 나온다.
|
|
53
|
+
return userId ? `<@${userId}> ${token.provider}` : `${label} (${token.provider})`;
|
|
54
|
+
})
|
|
55
|
+
.join("\n");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function sendDigest(userMap: Map<string, string>): Promise<void> {
|
|
59
|
+
const inactive = await TokenModel.findInactive("A");
|
|
60
|
+
if (inactive.length === 0) return;
|
|
61
|
+
|
|
62
|
+
await notifySlack({
|
|
63
|
+
title: `재로그인 필요 ${inactive.length}건`,
|
|
64
|
+
context: buildDigestContext(inactive, userMap),
|
|
65
|
+
color: SLACK_COLOR.bad,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let timer: ReturnType<typeof setInterval> | null = null;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* `SLACK_EXPIRY_DIGEST_INTERVAL_MINUTES` 가 양수일 때만 켜진다. 미설정·0 이면 비활성.
|
|
73
|
+
*
|
|
74
|
+
* 인스턴스가 여러 개면 각자 보낸다. 사망 알림과 달리 상태 변화가 없어 DB 로 억제할 수 없고,
|
|
75
|
+
* 다이제스트는 하루 몇 건 수준이라 중복을 감수하는 편이 잠금 장치를 두는 것보다 낫다.
|
|
76
|
+
*/
|
|
77
|
+
export function startExpiredTokenDigest(): void {
|
|
78
|
+
const minutes = Number(process.env.SLACK_EXPIRY_DIGEST_INTERVAL_MINUTES ?? 0);
|
|
79
|
+
if (!Number.isFinite(minutes) || minutes <= 0) return;
|
|
80
|
+
|
|
81
|
+
const userMap = parseUserMap(process.env.SLACK_USER_MAP);
|
|
82
|
+
logger.info(`expired token digest every ${minutes}m (${userMap.size} user mappings)`);
|
|
83
|
+
|
|
84
|
+
timer = setInterval(() => {
|
|
85
|
+
sendDigest(userMap).catch((e) => logger.warn(`digest failed: ${(e as Error).message}`));
|
|
86
|
+
}, minutes * MINUTE_MS);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function stopExpiredTokenDigest(): void {
|
|
90
|
+
if (!timer) return;
|
|
91
|
+
clearInterval(timer);
|
|
92
|
+
timer = null;
|
|
93
|
+
}
|
|
@@ -43,14 +43,11 @@ vi.mock("../token/token.model", () => ({
|
|
|
43
43
|
accountId: string | undefined,
|
|
44
44
|
saveParams: unknown,
|
|
45
45
|
) => {
|
|
46
|
-
let isNew = false;
|
|
47
46
|
if (accountId) {
|
|
48
47
|
const olds = (await findByAccountMock()) as unknown as { id: number; active: boolean }[];
|
|
49
48
|
if (olds.length > 0) await tokenDelMock();
|
|
50
|
-
else isNew = true;
|
|
51
49
|
}
|
|
52
50
|
await (tokenSaveMock as unknown as (rows: unknown[]) => Promise<number[]>)([saveParams]);
|
|
53
|
-
return { isNew };
|
|
54
51
|
},
|
|
55
52
|
},
|
|
56
53
|
}));
|
|
@@ -56,7 +56,7 @@ describe("QgridDispatcherClass", () => {
|
|
|
56
56
|
|
|
57
57
|
await expect(
|
|
58
58
|
dispatcher.query({ prompt: "hi", model: "anthropic/claude-sonnet-4-6" }),
|
|
59
|
-
).rejects.
|
|
59
|
+
).rejects.toMatchObject({ statusCode: 503 });
|
|
60
60
|
});
|
|
61
61
|
|
|
62
62
|
it("AnthropicDispatcher 미초기화 시 queryStream 은 delta 없는 query 폴백 없이 실패한다", async () => {
|
|
@@ -71,7 +71,35 @@ describe("QgridDispatcherClass", () => {
|
|
|
71
71
|
onError: vi.fn(),
|
|
72
72
|
},
|
|
73
73
|
),
|
|
74
|
-
).rejects.
|
|
74
|
+
).rejects.toMatchObject({ statusCode: 503 });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// 기동 중과 초기화 실패는 클라이언트의 재시도 판단이 갈리는 지점이라 상태코드로 구분한다.
|
|
78
|
+
it("기동 중 미준비는 503 으로 재시도 가능함을 알린다", async () => {
|
|
79
|
+
const dispatcher = new QgridDispatcherClass();
|
|
80
|
+
|
|
81
|
+
await expect(
|
|
82
|
+
dispatcher.query({ prompt: "hi", model: "openai/gpt-5.4" }),
|
|
83
|
+
).rejects.toMatchObject({ statusCode: 503 });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("초기화가 실패로 끝났으면 500 으로 재시도가 무의미함을 알린다", async () => {
|
|
87
|
+
const dispatcher = new QgridDispatcherClass();
|
|
88
|
+
dispatcher.startupState.openai = "failed";
|
|
89
|
+
|
|
90
|
+
await expect(
|
|
91
|
+
dispatcher.query({ prompt: "hi", model: "openai/gpt-5.4" }),
|
|
92
|
+
).rejects.toMatchObject({ statusCode: 500 });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("provider 별로 기동 상태를 따로 본다", async () => {
|
|
96
|
+
const dispatcher = new QgridDispatcherClass();
|
|
97
|
+
dispatcher.startupState.openai = "failed";
|
|
98
|
+
|
|
99
|
+
// openai 가 실패해도 anthropic 은 여전히 기동 중(재시도 가능)이다.
|
|
100
|
+
await expect(
|
|
101
|
+
dispatcher.query({ prompt: "hi", model: "anthropic/claude-sonnet-4-6" }),
|
|
102
|
+
).rejects.toMatchObject({ statusCode: 503 });
|
|
75
103
|
});
|
|
76
104
|
|
|
77
105
|
it("provider prefix 없는 모델은 AnthropicDispatcher 로 암묵 라우팅하지 않는다", async () => {
|
|
@@ -4,9 +4,13 @@
|
|
|
4
4
|
* - 메모리 캐시 (Map<id, TokenSubsetA>) 는 TokenSubscriber 가 pg LISTEN/NOTIFY 로 갱신
|
|
5
5
|
* - OpenAI/Anthropic 요청은 각 provider dispatcher 로만 실행
|
|
6
6
|
* - QuotaError 는 그대로 상위 전파
|
|
7
|
+
* - dispatcher 미준비는 기동 중(503)/초기화 실패(500)로 구분해 던진다
|
|
7
8
|
*/
|
|
8
9
|
|
|
10
|
+
import { InternalServerErrorException, ServiceUnavailableException } from "sonamu";
|
|
11
|
+
|
|
9
12
|
import { type JsonValue } from "../../codex-protocol/serde_json/JsonValue";
|
|
13
|
+
import { SD } from "../../i18n/sd.generated";
|
|
10
14
|
import { type AnthropicDispatcher } from "../../utils/providers/anthropic/anthropic-dispatcher";
|
|
11
15
|
import { getAccessToken } from "../../utils/providers/common/credentials";
|
|
12
16
|
import { calculateCostUsd } from "../../utils/providers/common/model-cost";
|
|
@@ -22,8 +26,14 @@ import { strictify } from "../../utils/providers/common/strictifier";
|
|
|
22
26
|
import { type OpenAIDispatcher } from "../../utils/providers/openai/openai-dispatcher";
|
|
23
27
|
import { type TokenSubsetA } from "../sonamu.generated";
|
|
24
28
|
import { decideConvRouting, issueConvContext } from "./conv-routing";
|
|
25
|
-
import {
|
|
26
|
-
|
|
29
|
+
import {
|
|
30
|
+
maskToken,
|
|
31
|
+
ProcessError,
|
|
32
|
+
type ProviderStartupState,
|
|
33
|
+
type QueryInput,
|
|
34
|
+
type QueryOutput,
|
|
35
|
+
type TokenStats,
|
|
36
|
+
} from "./qgrid.types";
|
|
27
37
|
import { type TokenSubscriber } from "./token-subscriber";
|
|
28
38
|
import { applyToolCallEmulation } from "./tool-emulation";
|
|
29
39
|
import { buildToolCallSchema } from "./tool-emulation-schema";
|
|
@@ -39,6 +49,34 @@ export class QgridDispatcherClass {
|
|
|
39
49
|
openaiDispatcher: OpenAIDispatcher | null = null;
|
|
40
50
|
anthropicDispatcher: AnthropicDispatcher | null = null;
|
|
41
51
|
|
|
52
|
+
/**
|
|
53
|
+
* provider 별 기동 상태. dispatcher 가 없는 이유를 구분하기 위해 필요하다 —
|
|
54
|
+
* HTTP 리스닝은 dispatcher 준비보다 먼저 열리므로(sonamu.config onStart),
|
|
55
|
+
* 그 사이 들어온 요청은 "잠시 후 되는" 상태와 "재시도해도 안 되는" 상태가 다르다.
|
|
56
|
+
*
|
|
57
|
+
* - `starting`: 워커 spawn 중. dev0 기준 25 워커 × 500ms 간격이라 1~2분 걸린다 → 재시도 가능
|
|
58
|
+
* - `ready`: 정상
|
|
59
|
+
* - `failed`: start() 가 예외로 끝남 → 재시도해도 같은 결과
|
|
60
|
+
*/
|
|
61
|
+
startupState: Record<"openai" | "anthropic", ProviderStartupState> = {
|
|
62
|
+
openai: "starting",
|
|
63
|
+
anthropic: "starting",
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* dispatcher 가 준비되지 않았을 때 던질 예외를 만든다.
|
|
68
|
+
*
|
|
69
|
+
* 기동 중이면 503(+Retry-After) 으로 재시도 가능함을 알리고, 초기화가 실패로 끝났으면
|
|
70
|
+
* 500 으로 재시도가 무의미함을 알린다. 이전에는 둘 다 QuotaError 였는데, 쿼터 소진이
|
|
71
|
+
* 아닌 상태를 그렇게 표현하면 호출자가 토큰 문제로 오해하고 재시도 판단도 못 한다.
|
|
72
|
+
*/
|
|
73
|
+
private notReadyError(provider: "openai" | "anthropic"): Error {
|
|
74
|
+
const label = provider === "openai" ? "OpenAI" : "Anthropic";
|
|
75
|
+
return this.startupState[provider] === "failed"
|
|
76
|
+
? new InternalServerErrorException(SD("qgrid.dispatcherFailed")(label))
|
|
77
|
+
: new ServiceUnavailableException(SD("qgrid.dispatcherStarting")(label));
|
|
78
|
+
}
|
|
79
|
+
|
|
42
80
|
countOf(name: string): number {
|
|
43
81
|
return this.requestCounts.get(name) ?? 0;
|
|
44
82
|
}
|
|
@@ -73,7 +111,7 @@ export class QgridDispatcherClass {
|
|
|
73
111
|
|
|
74
112
|
// provider prefix routing: 'openai/gpt-5.4' → OpenAIDispatcher
|
|
75
113
|
if (route.provider === "openai") {
|
|
76
|
-
if (!this.openaiDispatcher) throw
|
|
114
|
+
if (!this.openaiDispatcher) throw this.notReadyError("openai");
|
|
77
115
|
|
|
78
116
|
const decision = decideConvRouting(input);
|
|
79
117
|
const result = await this.openaiDispatcher.generate({
|
|
@@ -105,7 +143,7 @@ export class QgridDispatcherClass {
|
|
|
105
143
|
answerKind,
|
|
106
144
|
});
|
|
107
145
|
} else if (route.provider === "anthropic") {
|
|
108
|
-
if (!this.anthropicDispatcher) throw
|
|
146
|
+
if (!this.anthropicDispatcher) throw this.notReadyError("anthropic");
|
|
109
147
|
|
|
110
148
|
const decision = decideConvRouting(input);
|
|
111
149
|
const result = await this.anthropicDispatcher.generate({
|
|
@@ -142,7 +180,7 @@ export class QgridDispatcherClass {
|
|
|
142
180
|
const answerKind = input.jsonSchema ? ("json" as const) : ("text" as const);
|
|
143
181
|
|
|
144
182
|
if (route.provider === "openai") {
|
|
145
|
-
if (!this.openaiDispatcher) throw
|
|
183
|
+
if (!this.openaiDispatcher) throw this.notReadyError("openai");
|
|
146
184
|
|
|
147
185
|
const decision = decideConvRouting(input);
|
|
148
186
|
await this.openaiDispatcher.generateStream(
|
|
@@ -180,7 +218,7 @@ export class QgridDispatcherClass {
|
|
|
180
218
|
);
|
|
181
219
|
return;
|
|
182
220
|
} else if (route.provider === "anthropic") {
|
|
183
|
-
if (!this.anthropicDispatcher) throw
|
|
221
|
+
if (!this.anthropicDispatcher) throw this.notReadyError("anthropic");
|
|
184
222
|
|
|
185
223
|
const decision = decideConvRouting(input);
|
|
186
224
|
await this.anthropicDispatcher.generateStream(
|
|
@@ -482,6 +482,11 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
482
482
|
return { names: await RequestLogModel.distinctProjectNames() };
|
|
483
483
|
}
|
|
484
484
|
|
|
485
|
+
@api({ httpMethod: "GET", clients: ["axios", "tanstack-query"] })
|
|
486
|
+
async modelNames(): Promise<{ names: string[] }> {
|
|
487
|
+
return { names: await RequestLogModel.distinctModelNames() };
|
|
488
|
+
}
|
|
489
|
+
|
|
485
490
|
@api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
|
|
486
491
|
async addToken(
|
|
487
492
|
provider: string,
|
|
@@ -609,7 +614,7 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
609
614
|
pending.redirectUri,
|
|
610
615
|
);
|
|
611
616
|
|
|
612
|
-
|
|
617
|
+
await TokenModel.replaceByAccount("anthropic", tokens.accountUuid, {
|
|
613
618
|
provider: "anthropic",
|
|
614
619
|
credentials: {
|
|
615
620
|
accessToken: tokens.accessToken,
|
|
@@ -620,7 +625,7 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
620
625
|
name: pending.name,
|
|
621
626
|
});
|
|
622
627
|
|
|
623
|
-
|
|
628
|
+
notifyTokenAdded(pending.name, "anthropic");
|
|
624
629
|
}
|
|
625
630
|
|
|
626
631
|
async handleOAuthCallback(code: string, state: string, reply: FastifyReply): Promise<void> {
|
|
@@ -648,7 +653,7 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
648
653
|
QgridDispatcher.openaiDispatcher
|
|
649
654
|
.completeBrowserLogin()
|
|
650
655
|
.then(async (creds) => {
|
|
651
|
-
|
|
656
|
+
await TokenModel.replaceByAccount("openai", creds.accountId, {
|
|
652
657
|
provider: "openai",
|
|
653
658
|
credentials: {
|
|
654
659
|
accessToken: creds.accessToken,
|
|
@@ -660,7 +665,7 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
660
665
|
name,
|
|
661
666
|
});
|
|
662
667
|
logger.info(`OpenAI token saved for ${name}`);
|
|
663
|
-
|
|
668
|
+
notifyTokenAdded(name, "openai");
|
|
664
669
|
})
|
|
665
670
|
.catch((e) => {
|
|
666
671
|
logger.warn(`OpenAI browser login failed: ${(e as Error).message}`);
|
|
@@ -789,10 +794,14 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
789
794
|
|
|
790
795
|
@api({ httpMethod: "GET", clients: ["axios", "tanstack-query"] })
|
|
791
796
|
async health(): Promise<HealthResponse> {
|
|
797
|
+
const providers = { ...QgridDispatcher.startupState };
|
|
792
798
|
return {
|
|
793
799
|
status: "ok",
|
|
794
800
|
activeTokens: QgridDispatcher.tokens.size,
|
|
795
801
|
subscriber: QgridDispatcher.subscriber?.status() ?? null,
|
|
802
|
+
// provider 별 라우팅이라 하나만 준비돼도 그 provider 요청은 처리된다.
|
|
803
|
+
ready: Object.values(providers).includes("ready"),
|
|
804
|
+
providers,
|
|
796
805
|
};
|
|
797
806
|
}
|
|
798
807
|
}
|
|
@@ -306,10 +306,20 @@ export const SubscriberStatus = z.object({
|
|
|
306
306
|
});
|
|
307
307
|
export type SubscriberStatus = z.infer<typeof SubscriberStatus>;
|
|
308
308
|
|
|
309
|
+
export const ProviderStartupState = z.enum(["starting", "ready", "failed"]);
|
|
310
|
+
export type ProviderStartupState = z.infer<typeof ProviderStartupState>;
|
|
311
|
+
|
|
309
312
|
export const HealthResponse = z.object({
|
|
310
313
|
status: z.string(),
|
|
311
314
|
activeTokens: z.number(),
|
|
312
315
|
subscriber: SubscriberStatus.nullable(),
|
|
316
|
+
// HTTP 리스닝은 dispatcher 기동보다 먼저 열린다. 200 응답만으로는 요청을 처리할 수
|
|
317
|
+
// 있는지 알 수 없어 provider 별 준비 상태를 함께 노출한다.
|
|
318
|
+
ready: z.boolean(),
|
|
319
|
+
providers: z.object({
|
|
320
|
+
openai: ProviderStartupState,
|
|
321
|
+
anthropic: ProviderStartupState,
|
|
322
|
+
}),
|
|
313
323
|
});
|
|
314
324
|
export type HealthResponse = z.infer<typeof HealthResponse>;
|
|
315
325
|
|
|
@@ -38,7 +38,7 @@ describe("deactivateAuthDeadToken", () => {
|
|
|
38
38
|
|
|
39
39
|
it("deactivates and notifies when the failed attempt used the current refresh token", async () => {
|
|
40
40
|
findOneMock.mockResolvedValue(storedRefreshToken("rt-current"));
|
|
41
|
-
deactivateMock.mockResolvedValue(true);
|
|
41
|
+
deactivateMock.mockResolvedValue({ deactivated: true, keptAsLastActive: false });
|
|
42
42
|
|
|
43
43
|
await expect(
|
|
44
44
|
deactivateAuthDeadToken(token, "rt-current", "anthropic:400"),
|
|
@@ -67,7 +67,7 @@ describe("deactivateAuthDeadToken", () => {
|
|
|
67
67
|
|
|
68
68
|
it("does not notify when another process won the deactivation race", async () => {
|
|
69
69
|
findOneMock.mockResolvedValue(storedRefreshToken("rt-current"));
|
|
70
|
-
deactivateMock.mockResolvedValue(false);
|
|
70
|
+
deactivateMock.mockResolvedValue({ deactivated: false, keptAsLastActive: false });
|
|
71
71
|
|
|
72
72
|
await expect(deactivateAuthDeadToken(token, "rt-current", "anthropic:400")).resolves.toBe(
|
|
73
73
|
false,
|
|
@@ -76,6 +76,36 @@ describe("deactivateAuthDeadToken", () => {
|
|
|
76
76
|
expect(notifySlackMock).not.toHaveBeenCalled();
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
+
it("alerts instead of deactivating when the dying token is the last active one", async () => {
|
|
80
|
+
// 알림 쿨다운이 모듈 수준 상태라 다른 테스트의 발송 기록에 가려질 수 있다.
|
|
81
|
+
vi.resetModules();
|
|
82
|
+
const { deactivateAuthDeadToken: freshDeactivate } = await import("./token-death");
|
|
83
|
+
findOneMock.mockResolvedValue(storedRefreshToken("rt-current"));
|
|
84
|
+
deactivateMock.mockResolvedValue({ deactivated: false, keptAsLastActive: true });
|
|
85
|
+
|
|
86
|
+
await expect(freshDeactivate(token, "rt-current", "anthropic:400")).resolves.toBe(false);
|
|
87
|
+
|
|
88
|
+
expect(notifySlackMock).toHaveBeenCalledTimes(1);
|
|
89
|
+
const notification = notifySlackMock.mock.calls[0]![0] as { title: string; context: string };
|
|
90
|
+
expect(notification.title).toContain("마지막");
|
|
91
|
+
expect(notification.context).toContain("anthropic:400");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("suppresses repeat last-active alerts for the same provider within the cooldown", async () => {
|
|
95
|
+
// 이 경로는 토큰을 비활성화하지 않아 상태 변화가 없다 — 억제가 없으면 요청마다 알림이 간다.
|
|
96
|
+
// 쿨다운은 모듈 수준 상태라 앞 테스트의 발송 기록이 남는다. 새로 불러 격리한다.
|
|
97
|
+
vi.resetModules();
|
|
98
|
+
const { deactivateAuthDeadToken: freshDeactivate } = await import("./token-death");
|
|
99
|
+
findOneMock.mockResolvedValue(storedRefreshToken("rt-current"));
|
|
100
|
+
deactivateMock.mockResolvedValue({ deactivated: false, keptAsLastActive: true });
|
|
101
|
+
|
|
102
|
+
await freshDeactivate(token, "rt-current", "anthropic:400");
|
|
103
|
+
await freshDeactivate(token, "rt-current", "anthropic:400");
|
|
104
|
+
await freshDeactivate(token, "rt-current", "anthropic:401");
|
|
105
|
+
|
|
106
|
+
expect(notifySlackMock).toHaveBeenCalledTimes(1);
|
|
107
|
+
});
|
|
108
|
+
|
|
79
109
|
it("does nothing when the token no longer exists", async () => {
|
|
80
110
|
findOneMock.mockResolvedValue(undefined);
|
|
81
111
|
|
|
@@ -88,7 +118,7 @@ describe("deactivateAuthDeadToken", () => {
|
|
|
88
118
|
|
|
89
119
|
it("never puts the raw provider error body in the notification", async () => {
|
|
90
120
|
findOneMock.mockResolvedValue(storedRefreshToken("rt-current"));
|
|
91
|
-
deactivateMock.mockResolvedValue(true);
|
|
121
|
+
deactivateMock.mockResolvedValue({ deactivated: true, keptAsLastActive: false });
|
|
92
122
|
|
|
93
123
|
await deactivateAuthDeadToken(token, "rt-current", "anthropic:400");
|
|
94
124
|
|
|
@@ -42,8 +42,11 @@ export async function deactivateAuthDeadToken(
|
|
|
42
42
|
return false;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
const deactivated = await TokenModel.deactivateIfActive(token.id);
|
|
46
|
-
if (!deactivated)
|
|
45
|
+
const { deactivated, keptAsLastActive } = await TokenModel.deactivateIfActive(token.id);
|
|
46
|
+
if (!deactivated) {
|
|
47
|
+
if (keptAsLastActive) notifyLastActiveTokenDying(token, reasonCode);
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
47
50
|
|
|
48
51
|
logger.warn(`token deactivated (auth dead): ${token.name}(id=${token.id}) reason=${reasonCode}`);
|
|
49
52
|
void notifySlack({
|
|
@@ -55,7 +58,33 @@ export async function deactivateAuthDeadToken(
|
|
|
55
58
|
return true;
|
|
56
59
|
}
|
|
57
60
|
|
|
58
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* 마지막 활성 토큰이 죽었을 때의 알림 쿨다운.
|
|
63
|
+
*
|
|
64
|
+
* 이 경우 토큰은 비활성화되지 않고 살아남으므로, 뒤따르는 요청마다 같은 실패를 반복해
|
|
65
|
+
* 알림이 폭주한다. 비활성화 성공 경로가 조건부 UPDATE 로 중복을 막는 것과 달리 여기엔
|
|
66
|
+
* 상태 변화가 없어 DB 로 억제할 수 없다 — provider 단위로 시간 창을 둔다.
|
|
67
|
+
*/
|
|
68
|
+
const LAST_ACTIVE_ALERT_COOLDOWN_MS = 30 * 60 * 1000;
|
|
69
|
+
const lastActiveAlertAt = new Map<string, number>();
|
|
70
|
+
|
|
71
|
+
function notifyLastActiveTokenDying(token: DeadTokenRef, reasonCode: string): void {
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
const previous = lastActiveAlertAt.get(token.provider);
|
|
74
|
+
if (previous !== undefined && now - previous < LAST_ACTIVE_ALERT_COOLDOWN_MS) return;
|
|
75
|
+
lastActiveAlertAt.set(token.provider, now);
|
|
76
|
+
|
|
77
|
+
void notifySlack({
|
|
78
|
+
title: "마지막 토큰 사망",
|
|
79
|
+
subject: token.name,
|
|
80
|
+
context:
|
|
81
|
+
`${token.provider} · ${reasonCode} · 풀이 비지 않도록 유지 중입니다. ` +
|
|
82
|
+
`전 토큰이 동시에 실패했다면 client_id 취소나 OAuth 계약 변경을 의심하세요`,
|
|
83
|
+
color: SLACK_COLOR.bad,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 로그인이 완료돼 토큰이 저장됐을 때. 신규·재로그인을 가리지 않는다. */
|
|
59
88
|
export function notifyTokenAdded(name: string, provider: string): void {
|
|
60
89
|
logger.info(`token added: ${name} (${provider})`);
|
|
61
90
|
void notifySlack({
|