koishi-plugin-hhs-huatu 1.3.13 → 1.3.15
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/config.d.ts +1 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +209 -382
- package/lib/membershipSystem.d.ts +9 -1
- package/lib/services/opusQuota.d.ts +3 -1
- package/package.json +6 -6
- package/readme.md +21 -3
package/lib/config.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export declare const modelMap: {
|
|
|
12
12
|
readonly 'nai-v4-5-full': "nai-diffusion-4-5-full";
|
|
13
13
|
readonly 'nai-v5-curated': "nai-diffusion-5-curated";
|
|
14
14
|
readonly 'nai-v5-full': "nai-diffusion-5-full";
|
|
15
|
+
readonly 'nai-v5-full-medium': "nai-diffusion-5-full-medium";
|
|
15
16
|
};
|
|
16
17
|
export declare const orientMap: {
|
|
17
18
|
readonly landscape: {
|
package/lib/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Context } from 'koishi';
|
|
2
2
|
import { Config } from './config';
|
|
3
|
-
export declare const usage = "\n# \uD83C\uDFA8 hhs-huatu \u63D2\u4EF6\n\n> **\u57FA\u4E8E [novelai-bot](https://bot.novelai.dev/) \u7684\u589E\u5F3A\u7248 AI \u7ED8\u56FE\u63D2\u4EF6\uFF0C\u63D0\u4F9B\u66F4\u667A\u80FD\u3001\u4FBF\u6377\u7684 NovelAI \u4F53\u9A8C\u3002**\n\n[](https://qm.qq.com/q/4nKKvckKbu) [](https://github.com/hhs2275/koishi-plugin-hhs-huatu)\n\n### \u2728 \u6838\u5FC3\u4EAE\u70B9\n\u65B0\u7248\u672C\u6539\u7528\u6570\u636E\u5E93\u3002\u4F60\u7684\u4F1A\u5458\u6570\u636E\u5728 \u4F60\u7684 Koishi \u6839\u76EE\u5F55/data/hhs-huatu-user-data.json\u3002\u628A\u5B83\u653E\u5728hhs-huatu-import\u6587\u4EF6\u5939\u4E0B\u9762\uFF0C\u7136\u540E\u53D1\u201C\u4F1A\u5458\u8C03\u8BD5--import\u201D\u6307\u4EE4\uFF0C\u5C31\u53EF\u4EE5\u628A\u6570\u636E\u5BFC\u5165\u6570\u636E\u5E93\u3002\n\n\u672C\u63D2\u4EF6\u9488\u5BF9 **NovelAI V4 / V4.5 / V5** \u6A21\u578B\u8FDB\u884C\u4E86\u6DF1\u5EA6\u9002\u914D\uFF0C\u9884\u8BBE\u5FEB\u6377\u6307\u4EE4\uFF0C\u52A9\u4F60\u5FEB\u901F\u5207\u6362\u6A21\u578B\uFF1A\n\n| \u6307\u4EE4 | \u5BF9\u5E94\u6A21\u578B (Model) | \u8BF4\u660E |\n| :--- | :--- | :--- |\n| `nai4` | `nai-diffusion-4-full` | V4 \u5168\u91CF\u6A21\u578B |\n| `nai4c` | `nai-diffusion-4-curated` | V4 \u7CBE\u9009\u6A21\u578B |\n| `nai4-5` | `nai-diffusion-4-5-full` | V4.5 \u5168\u91CF\u6A21\u578B |\n| `nai4-5c` | `nai-diffusion-4-5-curated` | V4.5 \u7CBE\u9009\u6A21\u578B |\n| `nai5` | `nai-diffusion-5-full` | V5 \u5168\u91CF\u6A21\u578B |\n| `nai5c` | `nai-diffusion-5-curated` | V5 \u7CBE\u9009\u6A21\u578B |\n\n### \uD83D\uDEE0\uFE0F \u529F\u80FD\u5217\u8868\n\n**\uD83C\uDFA8 \u7ED8\u56FE\u6838\u5FC3**\n* **\u5168\u9762\u652F\u6301**\uFF1A\u6587\u751F\u56FE\u3001\u56FE\u751F\u56FE\u57FA\u7840\u529F\u80FD\u5B8C\u6574\u3002\n* **\u5C40\u90E8\u91CD\u7ED8 (Inpaint)**\uFF1A\u652F\u6301\u5BF9\u56FE\u7247\u7279\u5B9A\u533A\u57DF\u8FDB\u884C\u91CD\u7ED8\u3002\n* **\u5BFC\u6F14\u5DE5\u5177**\uFF1Anovelai\u5B98\u7F51\u7684\u5BFC\u6F14\u5DE5\u5177\u529F\u80FD\u3002\n* **V4 \u89D2\u8272\u63D0\u793A\u8BCD**\uFF1Anovelai\u5B98\u7F51\u7684\u591A\u89D2\u8272\u63D0\u793A\u8BCD\u7CFB\u7EDF\u3002\n\n**\u2699\uFE0F \u7CFB\u7EDF\u4E0E\u7BA1\u7406**\n* **\u9AD8\u5E76\u53D1\u4F18\u5316**\uFF1A\u5185\u7F6E\u961F\u5217\u7CFB\u7EDF\u4E0E Token \u6C60\u8F6E\u8BE2\uFF0C\u591A\u8D26\u53F7\u8D1F\u8F7D\u5747\u8861\u3002\n* **\u4F1A\u5458\u7BA1\u7406**\uFF1A\u652F\u6301\u7528\u6237\u5206\u7EA7\u7BA1\u7406\uFF1B\u53EF\u914D\u7F6E\u4EC5\u4F1A\u5458\u4F7F\u7528\u4F1A\u6D88\u8017\u914D\u989D\u7684 nai5 / nai5c\u3002\n* **\u667A\u80FD\u5BA1\u6838**\uFF1A\u96C6\u6210\u817E\u8BAF AI \u6216 API4AI\uFF0C\u81EA\u52A8\u8FC7\u6EE4\u8FDD\u89C4\u5185\u5BB9\u3002\n\n### \uD83D\uDDD3\uFE0F \u5F00\u53D1\u8BA1\u5212\n- [\u221A] **\u70B9\u6570\u63A7\u5236\u7CFB\u7EDF**\uFF1A\u6309 NovelAI \u5B98\u7F51\u524D\u7AEF\u516C\u5F0F\u9884\u6263\u672C\u5730\u70B9\u6570\uFF08V5 \u4E3A V4.5 \u7684 1.5 \u500D\uFF09\u3002\u5B9E\u9645\u6263\u8D39\u4EE5 NovelAI \u8D26\u6237\u4E3A\u51C6\u3002\n- [x] ~~**\u6C1B\u56F4\u4F20\u8F93\u529F\u80FD**\uFF1A\u5B9E\u73B0novelai\u5B98\u7F51\u7684\u6C1B\u56F4\u4F20\u8F93\u529F\u80FD\u3002v4\u7248\u672C\u7684\u98CE\u5473\u4F20\u8F93api\u4E0D\u5141\u8BB8\u76F4\u63A5\u53D1\u56FE\uFF0C\u9700\u8981\u5148\u53BB\u5B98\u7F51\u751F\u6210\u5BF9\u5E94\u6587\u4EF6\uFF0C\u6211\u505A\u4E0D\u5230\u3002~~\n- [\u221A] **\u89D2\u8272\u53C2\u8003\u529F\u80FD**\uFF1A\u5B9E\u73B0novelai\u5B98\u7F51\u7684\u89D2\u8272\u53C2\u8003\u529F\u80FD\u3002\uFF08\u5DF2\u88AB\u7CBE\u786E\u53C2\u8003\u66FF\u4EE3\uFF09\n---\n\n### \uD83D\uDCD6 \u66F4\u591A\u8D44\u6E90\n* \u8BE6\u7EC6\u6559\u7A0B\u8BF7\u79FB\u6B65 [GitHub \u4ED3\u5E93](https://github.com/hhs2275/koishi-plugin-hhs-huatu)\n* \u9047\u5230\u95EE\u9898\uFF1F\u6B22\u8FCE\u52A0\u5165\u4EA4\u6D41\u7FA4\uFF1A[112879548](https://qm.qq.com/q/4nKKvckKbu) (\u8054\u7CFB\u7FA4\u4E3B\u53CD\u9988\u95EE\u9898/\u63D0\u4EA4\u5EFA\u8BAE/\u6109\u5FEB\u73A9\u800D)\n";
|
|
3
|
+
export declare const usage = "\n# \uD83C\uDFA8 hhs-huatu \u63D2\u4EF6\n\n> **\u57FA\u4E8E [novelai-bot](https://bot.novelai.dev/) \u7684\u589E\u5F3A\u7248 AI \u7ED8\u56FE\u63D2\u4EF6\uFF0C\u63D0\u4F9B\u66F4\u667A\u80FD\u3001\u4FBF\u6377\u7684 NovelAI \u4F53\u9A8C\u3002**\n\n[](https://qm.qq.com/q/4nKKvckKbu) [](https://github.com/hhs2275/koishi-plugin-hhs-huatu)\n\n### \u2728 \u6838\u5FC3\u4EAE\u70B9\n\u65B0\u7248\u672C\u6539\u7528\u6570\u636E\u5E93\u3002\u4F60\u7684\u4F1A\u5458\u6570\u636E\u5728 \u4F60\u7684 Koishi \u6839\u76EE\u5F55/data/hhs-huatu-user-data.json\u3002\u628A\u5B83\u653E\u5728hhs-huatu-import\u6587\u4EF6\u5939\u4E0B\u9762\uFF0C\u7136\u540E\u53D1\u201C\u4F1A\u5458\u8C03\u8BD5--import\u201D\u6307\u4EE4\uFF0C\u5C31\u53EF\u4EE5\u628A\u6570\u636E\u5BFC\u5165\u6570\u636E\u5E93\u3002\n\n\u672C\u63D2\u4EF6\u9488\u5BF9 **NovelAI V4 / V4.5 / V5** \u6A21\u578B\u8FDB\u884C\u4E86\u6DF1\u5EA6\u9002\u914D\uFF0C\u9884\u8BBE\u5FEB\u6377\u6307\u4EE4\uFF0C\u52A9\u4F60\u5FEB\u901F\u5207\u6362\u6A21\u578B\uFF1A\n\n| \u6307\u4EE4 | \u5BF9\u5E94\u6A21\u578B (Model) | \u8BF4\u660E |\n| :--- | :--- | :--- |\n| `nai4` | `nai-diffusion-4-full` | V4 \u5168\u91CF\u6A21\u578B |\n| `nai4c` | `nai-diffusion-4-curated` | V4 \u7CBE\u9009\u6A21\u578B |\n| `nai4-5` | `nai-diffusion-4-5-full` | V4.5 \u5168\u91CF\u6A21\u578B |\n| `nai4-5c` | `nai-diffusion-4-5-curated` | V4.5 \u7CBE\u9009\u6A21\u578B |\n| `nai5` | `nai-diffusion-5-full` | V5 \u5168\u91CF\u6A21\u578B |\n| `nai5m` | `nai-diffusion-5-full-medium` | V5 Medium\uFF08Effort=Medium\uFF0C\u56FA\u5B9A 14 \u6B65\uFF0C\u6BCF\u6B21\u6263 0.6 \u6B21 nai5 \u989D\u5EA6\uFF09 |\n| `nai5c` | `nai-diffusion-5-curated` | V5 \u7CBE\u9009\u6A21\u578B |\n\n### \uD83D\uDEE0\uFE0F \u529F\u80FD\u5217\u8868\n\n**\uD83C\uDFA8 \u7ED8\u56FE\u6838\u5FC3**\n* **\u5168\u9762\u652F\u6301**\uFF1A\u6587\u751F\u56FE\u3001\u56FE\u751F\u56FE\u57FA\u7840\u529F\u80FD\u5B8C\u6574\u3002\n* **\u5C40\u90E8\u91CD\u7ED8 (Inpaint)**\uFF1A\u652F\u6301\u5BF9\u56FE\u7247\u7279\u5B9A\u533A\u57DF\u8FDB\u884C\u91CD\u7ED8\u3002\n* **\u5BFC\u6F14\u5DE5\u5177**\uFF1Anovelai\u5B98\u7F51\u7684\u5BFC\u6F14\u5DE5\u5177\u529F\u80FD\u3002\n* **V4 \u89D2\u8272\u63D0\u793A\u8BCD**\uFF1Anovelai\u5B98\u7F51\u7684\u591A\u89D2\u8272\u63D0\u793A\u8BCD\u7CFB\u7EDF\u3002\n\n**\u2699\uFE0F \u7CFB\u7EDF\u4E0E\u7BA1\u7406**\n* **\u9AD8\u5E76\u53D1\u4F18\u5316**\uFF1A\u5185\u7F6E\u961F\u5217\u7CFB\u7EDF\u4E0E Token \u6C60\u8F6E\u8BE2\uFF0C\u591A\u8D26\u53F7\u8D1F\u8F7D\u5747\u8861\u3002\n* **\u4F1A\u5458\u7BA1\u7406**\uFF1A\u652F\u6301\u7528\u6237\u5206\u7EA7\u7BA1\u7406\uFF1B\u53EF\u914D\u7F6E\u4EC5\u4F1A\u5458\u4F7F\u7528\u4F1A\u6D88\u8017\u914D\u989D\u7684 nai5 / nai5c\u3002\n* **\u667A\u80FD\u5BA1\u6838**\uFF1A\u96C6\u6210\u817E\u8BAF AI \u6216 API4AI\uFF0C\u81EA\u52A8\u8FC7\u6EE4\u8FDD\u89C4\u5185\u5BB9\u3002\n\n### \uD83D\uDDD3\uFE0F \u5F00\u53D1\u8BA1\u5212\n- [\u221A] **\u70B9\u6570\u63A7\u5236\u7CFB\u7EDF**\uFF1A\u6309 NovelAI \u5B98\u7F51\u524D\u7AEF\u516C\u5F0F\u9884\u6263\u672C\u5730\u70B9\u6570\uFF08V5 \u4E3A V4.5 \u7684 1.5 \u500D\uFF09\u3002\u5B9E\u9645\u6263\u8D39\u4EE5 NovelAI \u8D26\u6237\u4E3A\u51C6\u3002\n- [x] ~~**\u6C1B\u56F4\u4F20\u8F93\u529F\u80FD**\uFF1A\u5B9E\u73B0novelai\u5B98\u7F51\u7684\u6C1B\u56F4\u4F20\u8F93\u529F\u80FD\u3002v4\u7248\u672C\u7684\u98CE\u5473\u4F20\u8F93api\u4E0D\u5141\u8BB8\u76F4\u63A5\u53D1\u56FE\uFF0C\u9700\u8981\u5148\u53BB\u5B98\u7F51\u751F\u6210\u5BF9\u5E94\u6587\u4EF6\uFF0C\u6211\u505A\u4E0D\u5230\u3002~~\n- [\u221A] **\u89D2\u8272\u53C2\u8003\u529F\u80FD**\uFF1A\u5B9E\u73B0novelai\u5B98\u7F51\u7684\u89D2\u8272\u53C2\u8003\u529F\u80FD\u3002\uFF08\u5DF2\u88AB\u7CBE\u786E\u53C2\u8003\u66FF\u4EE3\uFF09\n---\n\n### \uD83D\uDCD6 \u66F4\u591A\u8D44\u6E90\n* \u8BE6\u7EC6\u6559\u7A0B\u8BF7\u79FB\u6B65 [GitHub \u4ED3\u5E93](https://github.com/hhs2275/koishi-plugin-hhs-huatu)\n* \u9047\u5230\u95EE\u9898\uFF1F\u6B22\u8FCE\u52A0\u5165\u4EA4\u6D41\u7FA4\uFF1A[112879548](https://qm.qq.com/q/4nKKvckKbu) (\u8054\u7CFB\u7FA4\u4E3B\u53CD\u9988\u95EE\u9898/\u63D0\u4EA4\u5EFA\u8BAE/\u6109\u5FEB\u73A9\u800D)\n";
|
|
4
4
|
export * from './config';
|
|
5
5
|
export declare const reactive = true;
|
|
6
6
|
export declare const name = "hhs-huatu";
|
package/lib/index.js
CHANGED
|
@@ -586,7 +586,8 @@ function modelSupportsCharacters(model) {
|
|
|
586
586
|
"nai-diffusion-4-5-curated",
|
|
587
587
|
"nai-diffusion-4-5-full",
|
|
588
588
|
"nai-diffusion-5-curated",
|
|
589
|
-
"nai-diffusion-5-full"
|
|
589
|
+
"nai-diffusion-5-full",
|
|
590
|
+
"nai-diffusion-5-full-medium"
|
|
590
591
|
];
|
|
591
592
|
return supportedModels.includes(model);
|
|
592
593
|
}
|
|
@@ -910,7 +911,8 @@ var modelMap = {
|
|
|
910
911
|
"nai-v4-5-curated": "nai-diffusion-4-5-curated",
|
|
911
912
|
"nai-v4-5-full": "nai-diffusion-4-5-full",
|
|
912
913
|
"nai-v5-curated": "nai-diffusion-5-curated",
|
|
913
|
-
"nai-v5-full": "nai-diffusion-5-full"
|
|
914
|
+
"nai-v5-full": "nai-diffusion-5-full",
|
|
915
|
+
"nai-v5-full-medium": "nai-diffusion-5-full-medium"
|
|
914
916
|
};
|
|
915
917
|
var orientMap = {
|
|
916
918
|
landscape: { height: 832, width: 1216 },
|
|
@@ -1263,6 +1265,10 @@ var Config = import_koishi2.Schema.intersect([
|
|
|
1263
1265
|
model: import_koishi2.Schema.const("nai-v5-full"),
|
|
1264
1266
|
...NAI4ParamConfig.dict
|
|
1265
1267
|
}),
|
|
1268
|
+
import_koishi2.Schema.object({
|
|
1269
|
+
model: import_koishi2.Schema.const("nai-v5-full-medium"),
|
|
1270
|
+
...NAI4ParamConfig.dict
|
|
1271
|
+
}),
|
|
1266
1272
|
import_koishi2.Schema.object({ sampler: sampler.createSchema(sampler.nai) })
|
|
1267
1273
|
]),
|
|
1268
1274
|
import_koishi2.Schema.object({ decrisper: import_koishi2.Schema.boolean().description("默认启用 decrisper") })
|
|
@@ -1352,7 +1358,6 @@ var Config = import_koishi2.Schema.intersect([
|
|
|
1352
1358
|
]).description("点数模式").default("periodic"),
|
|
1353
1359
|
pointsDefault: import_koishi2.Schema.number().description("会员默认初始点数").default(200),
|
|
1354
1360
|
pointsRefreshCycleDays: import_koishi2.Schema.number().description("刷新周期(天数),默认每30天刷新一次").default(30).min(1).max(365),
|
|
1355
|
-
pointsRefreshCheckTime: import_koishi2.Schema.string().description("每天检查并执行刷新的时刻(HH:MM,24小时制,本地时区)。实际刷新发生在「周期到期后的第一个该时刻」。").default("00:05"),
|
|
1356
1361
|
pointsRefreshAmount: import_koishi2.Schema.number().description("每次刷新恢复的点数").default(200),
|
|
1357
1362
|
pointsRefreshIncludeNonMember: import_koishi2.Schema.boolean().description("刷新范围是否包括非会员").default(false)
|
|
1358
1363
|
}).description("点数控制设置"),
|
|
@@ -1892,8 +1897,12 @@ async function fetchSubscription(ctx, config, token) {
|
|
|
1892
1897
|
});
|
|
1893
1898
|
}
|
|
1894
1899
|
__name(fetchSubscription, "fetchSubscription");
|
|
1900
|
+
function isNovelAIV5MediumModel(model) {
|
|
1901
|
+
return model === "nai-diffusion-5-full-medium";
|
|
1902
|
+
}
|
|
1903
|
+
__name(isNovelAIV5MediumModel, "isNovelAIV5MediumModel");
|
|
1895
1904
|
function isNovelAIV5Model(model) {
|
|
1896
|
-
return model === "nai-diffusion-5-curated" || model === "nai-diffusion-5-full";
|
|
1905
|
+
return model === "nai-diffusion-5-curated" || model === "nai-diffusion-5-full" || isNovelAIV5MediumModel(model);
|
|
1897
1906
|
}
|
|
1898
1907
|
__name(isNovelAIV5Model, "isNovelAIV5Model");
|
|
1899
1908
|
function isOpusQuotaExhausted(subscription) {
|
|
@@ -1934,7 +1943,6 @@ __name(formatOpusQuota, "formatOpusQuota");
|
|
|
1934
1943
|
// src/membershipSystem.ts
|
|
1935
1944
|
var MIN_TIER = 1;
|
|
1936
1945
|
var MAX_TIER = 5;
|
|
1937
|
-
var POINTS_REFRESH_TOLERANCE = 1e3;
|
|
1938
1946
|
var MembershipSystem = class {
|
|
1939
1947
|
constructor(ctx, config) {
|
|
1940
1948
|
this.ctx = ctx;
|
|
@@ -1944,15 +1952,13 @@ var MembershipSystem = class {
|
|
|
1944
1952
|
this.loadUserDataFromDB();
|
|
1945
1953
|
this.setupCleanupTask();
|
|
1946
1954
|
this.setupReminderTask();
|
|
1947
|
-
|
|
1948
|
-
ctx.accept(["membershipEnabled", "memberCleanupEnabled", "memberCleanupTime", "memberExpiryReminderEnabled", "memberReminderTime", "memberReminderHours", "pointsEnabled", "pointsMode", "pointsRefreshCycleDays", "pointsRefreshCheckTime"], () => {
|
|
1955
|
+
ctx.accept(["membershipEnabled", "memberCleanupEnabled", "memberCleanupTime", "memberExpiryReminderEnabled", "memberReminderTime", "memberReminderHours"], () => {
|
|
1949
1956
|
ctx.logger.info("会员系统配置已更新,重新安排定时任务");
|
|
1950
1957
|
if (this.config.membershipEnabled && Object.keys(this.userData).length === 0) {
|
|
1951
1958
|
this.loadUserDataFromDB().catch((err) => this.ctx.logger.error("动态加载用户数据失败", err));
|
|
1952
1959
|
}
|
|
1953
1960
|
this.setupCleanupTask();
|
|
1954
1961
|
this.setupReminderTask();
|
|
1955
|
-
this.setupPointsRefreshTask();
|
|
1956
1962
|
});
|
|
1957
1963
|
}
|
|
1958
1964
|
static {
|
|
@@ -1965,14 +1971,9 @@ var MembershipSystem = class {
|
|
|
1965
1971
|
// 定时任务取消函数
|
|
1966
1972
|
cleanupTimerDispose = null;
|
|
1967
1973
|
reminderTimerDispose = null;
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
pointsRefreshAnchor = 0;
|
|
1972
|
-
// 上次点数刷新的**实际**执行时刻(事实记录):手动刷新、停机补刷时可能与锚点不同。
|
|
1973
|
-
pointsRefreshLastActualAt = 0;
|
|
1974
|
-
// 库中记录仍是旧语义(lastUsed = 实际刷新时刻),加载后需要回写一次
|
|
1975
|
-
pointsRefreshStateNeedsMigration = false;
|
|
1974
|
+
// 上一版本的全局刷新时间,仅用于迁移旧数据
|
|
1975
|
+
legacyPointsRefreshTime = 0;
|
|
1976
|
+
pointsRefreshLocks = /* @__PURE__ */ new Map();
|
|
1976
1977
|
// 确保导入文件夹存在
|
|
1977
1978
|
ensureImportDir() {
|
|
1978
1979
|
try {
|
|
@@ -2000,8 +2001,11 @@ var MembershipSystem = class {
|
|
|
2000
2001
|
dailyLimit: "unsigned",
|
|
2001
2002
|
lastDrawTime: "unsigned",
|
|
2002
2003
|
points: "integer",
|
|
2003
|
-
|
|
2004
|
-
|
|
2004
|
+
pointsRefreshAt: "unsigned",
|
|
2005
|
+
pointsLastRefreshedAt: "unsigned",
|
|
2006
|
+
// nai5DailyUsage / nai5Bucket:nai5m(Medium)按 0.6 权重扣减,会出现小数,用 float
|
|
2007
|
+
nai5DailyUsage: "float",
|
|
2008
|
+
nai5Bucket: "float",
|
|
2005
2009
|
nai5BucketDay: "unsigned",
|
|
2006
2010
|
nai5DepositTier: "unsigned"
|
|
2007
2011
|
}, {
|
|
@@ -2030,10 +2034,7 @@ var MembershipSystem = class {
|
|
|
2030
2034
|
let loadedCount = 0;
|
|
2031
2035
|
for (const row of rows) {
|
|
2032
2036
|
if (row.visitorId === "_system_points_refresh") {
|
|
2033
|
-
|
|
2034
|
-
this.pointsRefreshAnchor = actualAt > 0 ? this.alignToCheck(actualAt - POINTS_REFRESH_TOLERANCE) : 0;
|
|
2035
|
-
this.pointsRefreshLastActualAt = row.lastDrawTime || actualAt || 0;
|
|
2036
|
-
this.pointsRefreshStateNeedsMigration = (row.lastUsed || 0) !== this.pointsRefreshAnchor || (row.lastDrawTime || 0) !== this.pointsRefreshLastActualAt;
|
|
2037
|
+
this.legacyPointsRefreshTime = row.lastUsed || 0;
|
|
2037
2038
|
continue;
|
|
2038
2039
|
}
|
|
2039
2040
|
loadedCount++;
|
|
@@ -2045,6 +2046,8 @@ var MembershipSystem = class {
|
|
|
2045
2046
|
dailyLimit: row.dailyLimit,
|
|
2046
2047
|
lastDrawTime: row.lastDrawTime,
|
|
2047
2048
|
points: row.points,
|
|
2049
|
+
pointsRefreshAt: row.pointsRefreshAt || 0,
|
|
2050
|
+
pointsLastRefreshedAt: row.pointsLastRefreshedAt || 0,
|
|
2048
2051
|
nai5DailyUsage: row.nai5DailyUsage || 0,
|
|
2049
2052
|
nai5Bucket: row.nai5Bucket || 0,
|
|
2050
2053
|
nai5BucketDay: row.nai5BucketDay || 0,
|
|
@@ -2054,19 +2057,7 @@ var MembershipSystem = class {
|
|
|
2054
2057
|
this.ctx.logger.info(`会员系统数据从数据库加载成功,共 ${loadedCount} 条记录`);
|
|
2055
2058
|
await this.loadCardsFromDB();
|
|
2056
2059
|
await this.migrateLegacyMembers();
|
|
2057
|
-
|
|
2058
|
-
await this.checkAndCatchUpPointsRefresh();
|
|
2059
|
-
} catch (err) {
|
|
2060
|
-
this.ctx.logger.error("点数刷新补刷失败", err);
|
|
2061
|
-
}
|
|
2062
|
-
if (this.config.pointsEnabled && this.config.pointsMode === "periodic") {
|
|
2063
|
-
if (this.pointsRefreshStateNeedsMigration) {
|
|
2064
|
-
await this.savePointsRefreshState();
|
|
2065
|
-
this.pointsRefreshStateNeedsMigration = false;
|
|
2066
|
-
this.ctx.logger.info(`点数刷新状态已迁移为周期锚点制:锚点 ${this.formatRefreshAnchor()},上次实际刷新 ${this.formatLastRefreshTime()}`);
|
|
2067
|
-
}
|
|
2068
|
-
this.ctx.logger.info(`点数刷新计划:周期锚点 ${this.formatRefreshAnchor()},上次实际刷新 ${this.formatLastRefreshTime()},下次刷新 ${this.formatNextRefreshTime()}(${this.getDaysUntilNextRefresh()} 天后)`);
|
|
2069
|
-
}
|
|
2060
|
+
await this.migratePointsRefreshPlans();
|
|
2070
2061
|
} catch (err) {
|
|
2071
2062
|
this.ctx.logger.error("从数据库加载会员系统数据失败", err);
|
|
2072
2063
|
}
|
|
@@ -2115,7 +2106,24 @@ var MembershipSystem = class {
|
|
|
2115
2106
|
this.ctx.logger.info(`会员等级迁移完成:已为 ${migrated} 位存量会员生成 Lv1 会员卡`);
|
|
2116
2107
|
}
|
|
2117
2108
|
}
|
|
2118
|
-
|
|
2109
|
+
/**
|
|
2110
|
+
* 将旧版全局刷新时间迁移为用户级刷新计划。
|
|
2111
|
+
* 迁移只补字段,不改变当前点数、会员卡或使用记录。
|
|
2112
|
+
*/
|
|
2113
|
+
async migratePointsRefreshPlans() {
|
|
2114
|
+
if (!this.legacyPointsRefreshTime || this.config.pointsMode !== "periodic") return;
|
|
2115
|
+
const cycleMs = (this.config.pointsRefreshCycleDays || 30) * 24 * 60 * 60 * 1e3;
|
|
2116
|
+
let changed = false;
|
|
2117
|
+
for (const userId in this.userData) {
|
|
2118
|
+
const user = this.userData[userId];
|
|
2119
|
+
const tier = this.computeActiveTier(user.cards || []);
|
|
2120
|
+
const isInScope = tier > 0 || this.config.pointsRefreshIncludeNonMember;
|
|
2121
|
+
if (!isInScope || user.pointsRefreshAt) continue;
|
|
2122
|
+
user.pointsRefreshAt = this.legacyPointsRefreshTime + cycleMs;
|
|
2123
|
+
changed = true;
|
|
2124
|
+
}
|
|
2125
|
+
if (changed) await this.saveUserData();
|
|
2126
|
+
}
|
|
2119
2127
|
async syncCardsToDB(userId) {
|
|
2120
2128
|
try {
|
|
2121
2129
|
const user = this.userData[userId];
|
|
@@ -2155,25 +2163,9 @@ var MembershipSystem = class {
|
|
|
2155
2163
|
this.ctx.logger.error("批量保存会员卡数据失败", err);
|
|
2156
2164
|
}
|
|
2157
2165
|
}
|
|
2158
|
-
|
|
2159
|
-
async
|
|
2160
|
-
|
|
2161
|
-
await this.ctx.database.upsert("hhs_huatu_user", [{
|
|
2162
|
-
visitorId: "_system_points_refresh",
|
|
2163
|
-
isMember: false,
|
|
2164
|
-
membershipExpiry: 0,
|
|
2165
|
-
dailyUsage: 0,
|
|
2166
|
-
// lastUsed 在该行语义 = 周期锚点(对齐到检查点)
|
|
2167
|
-
lastUsed: this.pointsRefreshAnchor,
|
|
2168
|
-
dailyLimit: 0,
|
|
2169
|
-
// lastDrawTime 在该行语义 = 上次实际刷新时刻
|
|
2170
|
-
lastDrawTime: this.pointsRefreshLastActualAt,
|
|
2171
|
-
points: 0,
|
|
2172
|
-
nai5DailyUsage: 0
|
|
2173
|
-
}], ["visitorId"]);
|
|
2174
|
-
} catch (err) {
|
|
2175
|
-
this.ctx.logger.error("保存点数刷新状态失败", err);
|
|
2176
|
-
}
|
|
2166
|
+
/** 旧版全局刷新记录仅保留用于读取迁移,不再写入。 */
|
|
2167
|
+
async saveLastPointsRefreshTime() {
|
|
2168
|
+
return;
|
|
2177
2169
|
}
|
|
2178
2170
|
// 将单个用户数据同步到数据库
|
|
2179
2171
|
async syncUserToDB(userId) {
|
|
@@ -2189,6 +2181,8 @@ var MembershipSystem = class {
|
|
|
2189
2181
|
dailyLimit: user.dailyLimit,
|
|
2190
2182
|
lastDrawTime: user.lastDrawTime || 0,
|
|
2191
2183
|
points: user.points || 0,
|
|
2184
|
+
pointsRefreshAt: user.pointsRefreshAt || 0,
|
|
2185
|
+
pointsLastRefreshedAt: user.pointsLastRefreshedAt || 0,
|
|
2192
2186
|
nai5DailyUsage: user.nai5DailyUsage || 0,
|
|
2193
2187
|
nai5Bucket: user.nai5Bucket || 0,
|
|
2194
2188
|
nai5BucketDay: user.nai5BucketDay || 0,
|
|
@@ -2214,6 +2208,8 @@ var MembershipSystem = class {
|
|
|
2214
2208
|
dailyLimit: user.dailyLimit,
|
|
2215
2209
|
lastDrawTime: user.lastDrawTime || 0,
|
|
2216
2210
|
points: user.points || 0,
|
|
2211
|
+
pointsRefreshAt: user.pointsRefreshAt || 0,
|
|
2212
|
+
pointsLastRefreshedAt: user.pointsLastRefreshedAt || 0,
|
|
2217
2213
|
nai5DailyUsage: user.nai5DailyUsage || 0,
|
|
2218
2214
|
nai5Bucket: user.nai5Bucket || 0,
|
|
2219
2215
|
nai5BucketDay: user.nai5BucketDay || 0,
|
|
@@ -2385,6 +2381,7 @@ var MembershipSystem = class {
|
|
|
2385
2381
|
if (this.config.pointsMode === "periodic") {
|
|
2386
2382
|
const refreshAmount = this.getTierBenefit(safeTier).pointsRefresh;
|
|
2387
2383
|
user.points = refreshAmount;
|
|
2384
|
+
user.pointsRefreshAt = now + (this.config.pointsRefreshCycleDays || 30) * 24 * 60 * 60 * 1e3;
|
|
2388
2385
|
this.ctx.logger.info(`[会员系统] 用户 ${userId} 首次成为 Lv${safeTier} 会员,点数已初始化为 ${refreshAmount}`);
|
|
2389
2386
|
} else if (!user.points) {
|
|
2390
2387
|
user.points = this.config.pointsDefault || 200;
|
|
@@ -2510,6 +2507,7 @@ var MembershipSystem = class {
|
|
|
2510
2507
|
async deductPoints(userId, amount) {
|
|
2511
2508
|
if (!this.config.pointsEnabled || amount <= 0) return 0;
|
|
2512
2509
|
this.ensureUserData(userId);
|
|
2510
|
+
await this.ensurePointsFresh(userId);
|
|
2513
2511
|
const user = this.userData[userId];
|
|
2514
2512
|
const currentPoints = user.points || 0;
|
|
2515
2513
|
if (currentPoints < amount) {
|
|
@@ -2525,6 +2523,7 @@ var MembershipSystem = class {
|
|
|
2525
2523
|
async refundPoints(userId, amount) {
|
|
2526
2524
|
if (!this.config.pointsEnabled || amount <= 0) return;
|
|
2527
2525
|
this.ensureUserData(userId);
|
|
2526
|
+
await this.ensurePointsFresh(userId);
|
|
2528
2527
|
const user = this.userData[userId];
|
|
2529
2528
|
user.points = (user.points || 0) + amount;
|
|
2530
2529
|
await this.syncUserToDB(userId);
|
|
@@ -2547,6 +2546,7 @@ var MembershipSystem = class {
|
|
|
2547
2546
|
let updatedCount = 0;
|
|
2548
2547
|
const now = Date.now();
|
|
2549
2548
|
for (const userId in this.userData) {
|
|
2549
|
+
await this.ensurePointsFresh(userId);
|
|
2550
2550
|
const user = this.userData[userId];
|
|
2551
2551
|
if (membersOnly && (!user.isMember || user.membershipExpiry < now)) {
|
|
2552
2552
|
continue;
|
|
@@ -2565,245 +2565,70 @@ var MembershipSystem = class {
|
|
|
2565
2565
|
const message = updatedCount > 0 ? `✅ 成功为 ${updatedCount} 位用户${action} ${Math.abs(amount)} 点数` : `⚠️ 没有符合条件的用户`;
|
|
2566
2566
|
return { count: updatedCount, message };
|
|
2567
2567
|
}
|
|
2568
|
-
/**
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2568
|
+
/** 确保用户在读取或扣除点数前完成到期的惰性刷新。 */
|
|
2569
|
+
async ensurePointsFresh(userId) {
|
|
2570
|
+
if (!this.config.pointsEnabled || this.config.pointsMode !== "periodic") return;
|
|
2571
|
+
this.ensureUserData(userId);
|
|
2572
|
+
const running = this.pointsRefreshLocks.get(userId);
|
|
2573
|
+
if (running) return running;
|
|
2574
|
+
const task = this.refreshUserPointsIfDue(userId).finally(() => {
|
|
2575
|
+
if (this.pointsRefreshLocks.get(userId) === task) this.pointsRefreshLocks.delete(userId);
|
|
2576
|
+
});
|
|
2577
|
+
this.pointsRefreshLocks.set(userId, task);
|
|
2578
|
+
return task;
|
|
2579
|
+
}
|
|
2580
|
+
async refreshUserPointsIfDue(userId) {
|
|
2581
|
+
const user = this.userData[userId];
|
|
2582
|
+
const now = Date.now();
|
|
2583
|
+
const cycleMs = (this.config.pointsRefreshCycleDays || 30) * 24 * 60 * 60 * 1e3;
|
|
2584
|
+
const tier = this.getActiveTier(userId);
|
|
2585
|
+
const inScope = tier > 0 || this.config.pointsRefreshIncludeNonMember;
|
|
2586
|
+
if (!user.pointsRefreshAt) {
|
|
2587
|
+
if (!inScope) return;
|
|
2588
|
+
user.pointsRefreshAt = now + cycleMs;
|
|
2589
|
+
await this.syncUserToDB(userId);
|
|
2590
|
+
return;
|
|
2591
|
+
}
|
|
2592
|
+
if (user.pointsRefreshAt > now || !inScope) return;
|
|
2593
|
+
const plannedAt = user.pointsRefreshAt;
|
|
2594
|
+
user.points = this.getTierBenefit(tier > 0 ? tier : 1).pointsRefresh;
|
|
2595
|
+
user.pointsLastRefreshedAt = now;
|
|
2596
|
+
do {
|
|
2597
|
+
user.pointsRefreshAt += cycleMs;
|
|
2598
|
+
} while (user.pointsRefreshAt <= now);
|
|
2599
|
+
await this.syncUserToDB(userId);
|
|
2600
|
+
this.ctx.logger.info(`[会员系统] 用户 ${userId} 惰性刷新点数:${plannedAt} → ${user.pointsRefreshAt}`);
|
|
2601
|
+
}
|
|
2602
|
+
/** 手动刷新点数,不改变用户自动刷新计划。 */
|
|
2574
2603
|
async refreshPoints(tierFilter) {
|
|
2575
2604
|
if (!this.config.pointsEnabled || this.config.pointsMode !== "periodic") {
|
|
2576
2605
|
return { count: 0, message: "点数刷新未启用或不是周期模式" };
|
|
2577
2606
|
}
|
|
2578
2607
|
let refreshedCount = 0;
|
|
2579
|
-
const now = Date.now();
|
|
2580
2608
|
for (const userId in this.userData) {
|
|
2581
|
-
const
|
|
2582
|
-
|
|
2583
|
-
if (tier > 1 && this.config.tierEnabled === false) tier = 1;
|
|
2584
|
-
const isActive = tier > 0;
|
|
2585
|
-
if (!this.config.pointsRefreshIncludeNonMember && !isActive) {
|
|
2586
|
-
continue;
|
|
2587
|
-
}
|
|
2609
|
+
const tier = this.getActiveTier(userId);
|
|
2610
|
+
if (tier <= 0 && !this.config.pointsRefreshIncludeNonMember) continue;
|
|
2588
2611
|
if (tierFilter !== void 0 && tier !== tierFilter) continue;
|
|
2612
|
+
await this.ensurePointsFresh(userId);
|
|
2613
|
+
const user = this.userData[userId];
|
|
2589
2614
|
user.points = this.getTierBenefit(tier > 0 ? tier : 1).pointsRefresh;
|
|
2615
|
+
await this.syncUserToDB(userId);
|
|
2590
2616
|
refreshedCount++;
|
|
2591
2617
|
}
|
|
2592
|
-
this.pointsRefreshLastActualAt = now;
|
|
2593
|
-
await this.savePointsRefreshState();
|
|
2594
|
-
if (refreshedCount > 0) {
|
|
2595
|
-
await this.saveUserData();
|
|
2596
|
-
}
|
|
2597
2618
|
const scope = tierFilter !== void 0 ? `Lv${tierFilter} 会员` : "用户";
|
|
2598
|
-
const message = refreshedCount > 0 ? `✅
|
|
2619
|
+
const message = refreshedCount > 0 ? `✅ 已手动刷新 ${refreshedCount} 位${scope}的点数,自动刷新计划未改变` : "⚠️ 没有符合条件的用户需要刷新点数";
|
|
2599
2620
|
this.ctx.logger.info(message);
|
|
2600
2621
|
return { count: refreshedCount, message };
|
|
2601
2622
|
}
|
|
2602
|
-
|
|
2603
|
-
/** 周期毫秒数 */
|
|
2604
|
-
getPointsRefreshCycleMs() {
|
|
2605
|
-
return (this.config.pointsRefreshCycleDays || 30) * 24 * 60 * 60 * 1e3;
|
|
2606
|
-
}
|
|
2607
|
-
/** 每日检查时刻(HH:MM,默认 00:05);配置非法时回落 00:05 */
|
|
2608
|
-
getCheckTimeParts() {
|
|
2609
|
-
const raw = String(this.config.pointsRefreshCheckTime || "00:05").trim();
|
|
2610
|
-
const m = raw.match(/^(\d{1,2}):(\d{1,2})$/);
|
|
2611
|
-
if (!m) return { hour: 0, minute: 5 };
|
|
2612
|
-
const hour = Number(m[1]);
|
|
2613
|
-
const minute = Number(m[2]);
|
|
2614
|
-
if (!Number.isInteger(hour) || !Number.isInteger(minute) || hour < 0 || hour > 23 || minute < 0 || minute > 59) {
|
|
2615
|
-
return { hour: 0, minute: 5 };
|
|
2616
|
-
}
|
|
2617
|
-
return { hour, minute };
|
|
2618
|
-
}
|
|
2619
|
-
/** 指定日期(本地时区)的检查点时刻 */
|
|
2620
|
-
checkPointOfDay(timestamp) {
|
|
2621
|
-
const { hour, minute } = this.getCheckTimeParts();
|
|
2622
|
-
const d = new Date(timestamp);
|
|
2623
|
-
return new Date(d.getFullYear(), d.getMonth(), d.getDate(), hour, minute, 0, 0).getTime();
|
|
2624
|
-
}
|
|
2625
|
-
/** 首个 ≥ timestamp 的检查点 */
|
|
2626
|
-
alignToCheck(timestamp) {
|
|
2627
|
-
const check = this.checkPointOfDay(timestamp);
|
|
2628
|
-
return check < timestamp ? check + 24 * 60 * 60 * 1e3 : check;
|
|
2629
|
-
}
|
|
2630
|
-
/** 最近一个 ≤ timestamp 的检查点 */
|
|
2631
|
-
lastCheckPointAtOrBefore(timestamp) {
|
|
2632
|
-
const check = this.checkPointOfDay(timestamp);
|
|
2633
|
-
return check > timestamp ? check - 24 * 60 * 60 * 1e3 : check;
|
|
2634
|
-
}
|
|
2635
|
-
/**
|
|
2636
|
-
* 下次刷新的**计划**时刻(毫秒);0 表示尚未初始化(首次运行,将在下一个检查点刷新)。
|
|
2637
|
-
* 计划时刻可能已经过去(例如停机漏检、或运维把锚点设到了过去),此时由 isPointsRefreshOverdue() 反映。
|
|
2638
|
-
*/
|
|
2639
|
-
getPointsRefreshPlan() {
|
|
2640
|
-
if (this.pointsRefreshAnchor === 0) return 0;
|
|
2641
|
-
return this.alignToCheck(this.pointsRefreshAnchor + this.getPointsRefreshCycleMs());
|
|
2642
|
-
}
|
|
2643
|
-
/**
|
|
2644
|
-
* 下次刷新**实际**会发生的时刻(毫秒时间戳);-1 表示未启用周期刷新。
|
|
2645
|
-
* 展示(会员 -u / 会员调试 -s)与执行判定都以这里为准:
|
|
2646
|
-
* - 尚未初始化(锚点 0)→ 下一个检查点(首次刷新)
|
|
2647
|
-
* - 计划时刻还没到 → 计划时刻
|
|
2648
|
-
* - 计划已过(停机漏检 / 手动把锚点设到过去)→ 下一个检查点(届时会补刷)
|
|
2649
|
-
*/
|
|
2650
|
-
getNextRefreshTime() {
|
|
2623
|
+
getNextPointsRefreshAt(userId) {
|
|
2651
2624
|
if (!this.config.pointsEnabled || this.config.pointsMode !== "periodic") return -1;
|
|
2652
|
-
|
|
2653
|
-
const plan = this.getPointsRefreshPlan();
|
|
2654
|
-
if (plan === 0) return this.alignToCheck(now);
|
|
2655
|
-
return plan <= now + POINTS_REFRESH_TOLERANCE ? this.alignToCheck(now + POINTS_REFRESH_TOLERANCE) : plan;
|
|
2656
|
-
}
|
|
2657
|
-
/** 是否已逾期(计划刷新时刻已过但尚未刷新):仅在停机漏检或手动把锚点设到过去时成立 */
|
|
2658
|
-
isPointsRefreshOverdue() {
|
|
2659
|
-
const plan = this.getPointsRefreshPlan();
|
|
2660
|
-
if (plan === 0) return false;
|
|
2661
|
-
return Date.now() + POINTS_REFRESH_TOLERANCE >= plan;
|
|
2662
|
-
}
|
|
2663
|
-
// 把时间戳格式化为本地时区的 YYYY-MM-DD HH:mm(不依赖 locale,输出稳定)
|
|
2664
|
-
formatTimeValue(timestamp) {
|
|
2665
|
-
const d = new Date(timestamp);
|
|
2666
|
-
const p = /* @__PURE__ */ __name((n) => String(n).padStart(2, "0"), "p");
|
|
2667
|
-
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
|
2668
|
-
}
|
|
2669
|
-
/** 下次刷新时刻的展示文本(如 2026-11-05 00:05);未启用周期刷新时返回空串 */
|
|
2670
|
-
formatNextRefreshTime() {
|
|
2671
|
-
const t = this.getNextRefreshTime();
|
|
2672
|
-
return t < 0 ? "" : this.formatTimeValue(t);
|
|
2673
|
-
}
|
|
2674
|
-
/** 上次**实际**刷新时刻的展示文本;从未刷新过时返回「尚未记录」 */
|
|
2675
|
-
formatLastRefreshTime() {
|
|
2676
|
-
return this.pointsRefreshLastActualAt === 0 ? "尚未记录" : this.formatTimeValue(this.pointsRefreshLastActualAt);
|
|
2677
|
-
}
|
|
2678
|
-
/** 周期锚点的展示文本;尚未初始化时返回「尚未初始化」 */
|
|
2679
|
-
formatRefreshAnchor() {
|
|
2680
|
-
return this.pointsRefreshAnchor === 0 ? "尚未初始化" : this.formatTimeValue(this.pointsRefreshAnchor);
|
|
2681
|
-
}
|
|
2682
|
-
/** 下次刷新**计划**时刻的展示文本(可能已过去);尚未初始化时返回「尚未初始化」 */
|
|
2683
|
-
formatRefreshPlanTime() {
|
|
2684
|
-
const plan = this.getPointsRefreshPlan();
|
|
2685
|
-
return plan === 0 ? "尚未初始化" : this.formatTimeValue(plan);
|
|
2686
|
-
}
|
|
2687
|
-
/** 距离下次刷新的天数(用于「N 天后」文案) */
|
|
2688
|
-
getDaysUntilNextRefresh() {
|
|
2689
|
-
const next = this.getNextRefreshTime();
|
|
2690
|
-
if (next < 0) return -1;
|
|
2691
|
-
return Math.max(0, Math.ceil((next - Date.now()) / (24 * 60 * 60 * 1e3)));
|
|
2692
|
-
}
|
|
2693
|
-
/**
|
|
2694
|
-
* 刷新完成后推进周期锚点:锚点 = 本次刷新所属的检查点(取本次刷新时刻附近的检查点),
|
|
2695
|
-
* 而不是"实际执行时刻"。因此:
|
|
2696
|
-
* - 正常在检查点触发 → 锚点就是该检查点,周期保持对齐;
|
|
2697
|
-
* - 停机补刷(白天启动)→ 锚点仍是当天的检查点,周期不会漂移到白天;
|
|
2698
|
-
* - 定时器早/晚几十毫秒触发 → 仍归到同一个检查点,不会退/进一天。
|
|
2699
|
-
*/
|
|
2700
|
-
advanceAnchorAfterRefresh() {
|
|
2701
|
-
this.pointsRefreshAnchor = this.lastCheckPointAtOrBefore(Date.now() + POINTS_REFRESH_TOLERANCE);
|
|
2702
|
-
}
|
|
2703
|
-
/**
|
|
2704
|
-
* 停机漏检补刷(S1):启动加载完成后若计划时刻已过(说明实例在检查点离线),立即补刷一次,
|
|
2705
|
-
* 并把锚点推进到"本次补刷所属的检查点"。返回是否执行了补刷。
|
|
2706
|
-
*/
|
|
2707
|
-
async checkAndCatchUpPointsRefresh() {
|
|
2708
|
-
if (!this.config.membershipEnabled || !this.config.pointsEnabled || this.config.pointsMode !== "periodic") return false;
|
|
2709
|
-
if (!this.isPointsRefreshOverdue()) return false;
|
|
2710
|
-
const plan = this.getPointsRefreshPlan();
|
|
2711
|
-
this.ctx.logger.info(`检测到点数刷新计划已逾期(计划 ${this.formatTimeValue(plan)},当前 ${this.formatTimeValue(Date.now())}),正在补刷...`);
|
|
2712
|
-
const result = await this.refreshPoints();
|
|
2713
|
-
this.advanceAnchorAfterRefresh();
|
|
2714
|
-
await this.savePointsRefreshState();
|
|
2715
|
-
this.ctx.logger.info(`点数刷新补刷完成:${result.message};周期锚点推进至 ${this.formatTimeValue(this.pointsRefreshAnchor)},下次刷新 ${this.formatTimeValue(this.getNextRefreshTime())}`);
|
|
2716
|
-
return true;
|
|
2717
|
-
}
|
|
2718
|
-
// ========== 点数刷新:运维接口(查看 / 设置锚点与计划时刻,均不触发刷新) ==========
|
|
2719
|
-
/** 周期锚点时间戳(0 = 尚未初始化) */
|
|
2720
|
-
getPointsRefreshAnchor() {
|
|
2721
|
-
return this.pointsRefreshAnchor;
|
|
2722
|
-
}
|
|
2723
|
-
/** 下次刷新的计划时刻(可能已过);0 = 尚未初始化 */
|
|
2724
|
-
getPointsRefreshPlanTime() {
|
|
2725
|
-
return this.getPointsRefreshPlan();
|
|
2625
|
+
return this.userData[userId]?.pointsRefreshAt || 0;
|
|
2726
2626
|
}
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
* 解析运维输入的时间:
|
|
2733
|
-
* - `now`:当前时刻
|
|
2734
|
-
* - `+Nd` / `+Nh` / `+Nm`:相对当前时刻 N 天/小时/分钟
|
|
2735
|
-
* - `YYYY-MM-DD`:该日期(dateOnly=true,调用方按"当天的检查点"解释)
|
|
2736
|
-
* - `YYYY-MM-DD HH:mm` / `YYYY-MM-DDTHH:mm`:精确到分
|
|
2737
|
-
* 解析失败返回 null。
|
|
2738
|
-
*/
|
|
2739
|
-
parseRefreshTimeInput(input) {
|
|
2740
|
-
const s = String(input ?? "").trim();
|
|
2741
|
-
if (!s) return null;
|
|
2742
|
-
const now = Date.now();
|
|
2743
|
-
if (s === "now" || s === "现在") return { timestamp: now, dateOnly: false };
|
|
2744
|
-
const rel = s.match(/^\+(\d+)([dhm])$/i);
|
|
2745
|
-
if (rel) {
|
|
2746
|
-
const n = Number(rel[1]);
|
|
2747
|
-
const unit = rel[2].toLowerCase();
|
|
2748
|
-
const unitMs = unit === "d" ? 864e5 : unit === "h" ? 36e5 : 6e4;
|
|
2749
|
-
return { timestamp: now + n * unitMs, dateOnly: false };
|
|
2750
|
-
}
|
|
2751
|
-
const abs = s.match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:[ T](\d{1,2}):(\d{1,2}))?$/);
|
|
2752
|
-
if (abs) {
|
|
2753
|
-
const year = Number(abs[1]);
|
|
2754
|
-
const month = Number(abs[2]);
|
|
2755
|
-
const day = Number(abs[3]);
|
|
2756
|
-
const hour = abs[4] ? Number(abs[4]) : 0;
|
|
2757
|
-
const minute = abs[5] ? Number(abs[5]) : 0;
|
|
2758
|
-
if (month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || minute > 59) return null;
|
|
2759
|
-
const d = new Date(year, month - 1, day, hour, minute, 0, 0);
|
|
2760
|
-
if (d.getFullYear() !== year || d.getMonth() !== month - 1 || d.getDate() !== day) return null;
|
|
2761
|
-
return { timestamp: d.getTime(), dateOnly: !abs[4] };
|
|
2762
|
-
}
|
|
2763
|
-
return null;
|
|
2764
|
-
}
|
|
2765
|
-
/**
|
|
2766
|
-
* 设置周期锚点(不触发刷新)。
|
|
2767
|
-
* - 输入仅日期 → 取该日期的检查点
|
|
2768
|
-
* - 输入含时间 → 取"最近一个 ≤ 该时刻的检查点"
|
|
2769
|
-
*/
|
|
2770
|
-
async setPointsRefreshAnchor(input) {
|
|
2771
|
-
if (!this.config.pointsEnabled || this.config.pointsMode !== "periodic") return "❌ 未启用周期刷新,无法设置周期锚点";
|
|
2772
|
-
const parsed = this.parseRefreshTimeInput(input);
|
|
2773
|
-
if (!parsed) return `❌ 时间格式无法识别:${input}(支持 now、+5d、2026-11-01、2026-11-01 00:05)`;
|
|
2774
|
-
const anchor = parsed.dateOnly ? this.checkPointOfDay(parsed.timestamp) : this.lastCheckPointAtOrBefore(parsed.timestamp);
|
|
2775
|
-
this.pointsRefreshAnchor = anchor;
|
|
2776
|
-
await this.savePointsRefreshState();
|
|
2777
|
-
this.ctx.logger.info(`[会员系统] 周期锚点被设置为 ${this.formatTimeValue(anchor)}(输入:${input})`);
|
|
2778
|
-
const next = this.getNextRefreshTime();
|
|
2779
|
-
return `✅ 周期锚点已设为 ${this.formatTimeValue(anchor)}(已对齐检查点)
|
|
2780
|
-
下次刷新:${this.formatTimeValue(next)}(${this.getDaysUntilNextRefresh()}天后)${this.isPointsRefreshOverdue() ? "\n⚠️ 该计划已逾期,将在下一个检查点自动补刷(也可用 -f 立即刷新)" : ""}`;
|
|
2781
|
-
}
|
|
2782
|
-
/** 直接指定下次刷新时刻(不触发刷新):取"首个 ≥ 该时刻的检查点",锚点由它反推 */
|
|
2783
|
-
async setPointsRefreshNextAt(input) {
|
|
2784
|
-
if (!this.config.pointsEnabled || this.config.pointsMode !== "periodic") return "❌ 未启用周期刷新,无法设置下次刷新时刻";
|
|
2785
|
-
const parsed = this.parseRefreshTimeInput(input);
|
|
2786
|
-
if (!parsed) return `❌ 时间格式无法识别:${input}(支持 now、+5d、2026-11-01、2026-11-01 00:05)`;
|
|
2787
|
-
const nextAt = parsed.dateOnly ? this.checkPointOfDay(parsed.timestamp) : this.alignToCheck(parsed.timestamp);
|
|
2788
|
-
const anchor = nextAt - this.getPointsRefreshCycleMs();
|
|
2789
|
-
if (anchor <= 0) return "❌ 目标时刻过于久远,无法反推周期锚点";
|
|
2790
|
-
this.pointsRefreshAnchor = anchor;
|
|
2791
|
-
await this.savePointsRefreshState();
|
|
2792
|
-
this.ctx.logger.info(`[会员系统] 下次刷新时刻被设置为 ${this.formatTimeValue(nextAt)}(输入:${input})`);
|
|
2793
|
-
return `✅ 下次刷新已设为 ${this.formatTimeValue(nextAt)}(周期锚点反推为 ${this.formatTimeValue(anchor)})
|
|
2794
|
-
本次设置不会立即刷新点数,如需立即刷新请加 -f`;
|
|
2795
|
-
}
|
|
2796
|
-
/** 重开一个周期:以当前检查点为锚点(不触发刷新) */
|
|
2797
|
-
async resetPointsRefreshCycle() {
|
|
2798
|
-
if (!this.config.pointsEnabled || this.config.pointsMode !== "periodic") return "❌ 未启用周期刷新,无法重置周期";
|
|
2799
|
-
const anchor = this.lastCheckPointAtOrBefore(Date.now() + POINTS_REFRESH_TOLERANCE);
|
|
2800
|
-
this.pointsRefreshAnchor = anchor;
|
|
2801
|
-
await this.savePointsRefreshState();
|
|
2802
|
-
this.ctx.logger.info(`[会员系统] 周期已重置,锚点 ${this.formatTimeValue(anchor)}`);
|
|
2803
|
-
const next = this.getNextRefreshTime();
|
|
2804
|
-
return `✅ 刷新周期已重置,周期锚点:${this.formatTimeValue(anchor)}
|
|
2805
|
-
下次刷新:${this.formatTimeValue(next)}(${this.getDaysUntilNextRefresh()}天后)
|
|
2806
|
-
本次操作不会立即刷新点数,如需立即刷新请加 -f`;
|
|
2627
|
+
getDaysUntilNextRefresh(userId) {
|
|
2628
|
+
if (!this.config.pointsEnabled || this.config.pointsMode !== "periodic") return -1;
|
|
2629
|
+
const at = userId ? this.getNextPointsRefreshAt(userId) : 0;
|
|
2630
|
+
if (!at) return 0;
|
|
2631
|
+
return Math.max(0, Math.ceil((at - Date.now()) / (24 * 60 * 60 * 1e3)));
|
|
2807
2632
|
}
|
|
2808
2633
|
/**
|
|
2809
2634
|
* 从 JSON 文件导入数据到数据库。
|
|
@@ -3050,39 +2875,7 @@ var MembershipSystem = class {
|
|
|
3050
2875
|
scheduleReminder();
|
|
3051
2876
|
}
|
|
3052
2877
|
}
|
|
3053
|
-
//
|
|
3054
|
-
setupPointsRefreshTask() {
|
|
3055
|
-
if (this.pointsRefreshTimerDispose) {
|
|
3056
|
-
this.pointsRefreshTimerDispose();
|
|
3057
|
-
this.pointsRefreshTimerDispose = null;
|
|
3058
|
-
}
|
|
3059
|
-
if (this.config.membershipEnabled && this.config.pointsEnabled && this.config.pointsMode === "periodic") {
|
|
3060
|
-
const cycleDays = this.config.pointsRefreshCycleDays || 30;
|
|
3061
|
-
const { hour, minute } = this.getCheckTimeParts();
|
|
3062
|
-
const checkTime = `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
|
|
3063
|
-
const scheduleDailyCheck = /* @__PURE__ */ __name(() => {
|
|
3064
|
-
const delay = this.getDelayUntilTime(checkTime);
|
|
3065
|
-
this.ctx.logger.info(`点数刷新检查任务已安排,将在 ${new Date(Date.now() + delay).toLocaleString()} 检查(检查时刻 ${checkTime},刷新周期 ${cycleDays} 天)`);
|
|
3066
|
-
this.pointsRefreshTimerDispose = this.ctx.setTimeout(async () => {
|
|
3067
|
-
const firstRun = this.pointsRefreshAnchor === 0;
|
|
3068
|
-
if (firstRun || this.isPointsRefreshOverdue()) {
|
|
3069
|
-
this.ctx.logger.info(firstRun ? "点数刷新尚未初始化,正在执行首次刷新..." : `已达到刷新周期 (${cycleDays} 天),正在执行点数刷新...`);
|
|
3070
|
-
await this.refreshPoints();
|
|
3071
|
-
this.advanceAnchorAfterRefresh();
|
|
3072
|
-
await this.savePointsRefreshState();
|
|
3073
|
-
this.ctx.logger.info(`本轮刷新完成,周期锚点 ${this.formatTimeValue(this.pointsRefreshAnchor)},下次刷新 ${this.formatTimeValue(this.getNextRefreshTime())}`);
|
|
3074
|
-
} else {
|
|
3075
|
-
const remainDays = this.getDaysUntilNextRefresh();
|
|
3076
|
-
if (this.config.debugLog) {
|
|
3077
|
-
this.ctx.logger.info(`点数刷新检查:距离下次刷新还有 ${remainDays} 天(${this.formatTimeValue(this.getNextRefreshTime())})`);
|
|
3078
|
-
}
|
|
3079
|
-
}
|
|
3080
|
-
scheduleDailyCheck();
|
|
3081
|
-
}, delay);
|
|
3082
|
-
}, "scheduleDailyCheck");
|
|
3083
|
-
scheduleDailyCheck();
|
|
3084
|
-
}
|
|
3085
|
-
}
|
|
2878
|
+
// 点数刷新采用按用户惰性执行,不注册全局定时任务。
|
|
3086
2879
|
// 给所有有效会员卡增加天数(按卡粒度,每张有效卡独立延长)。
|
|
3087
2880
|
// @param tierFilter 指定生效等级时仅操作该等级会员;缺省操作全部会员
|
|
3088
2881
|
async addDaysToAllMembers(days, tierFilter) {
|
|
@@ -3124,6 +2917,8 @@ var MembershipSystem = class {
|
|
|
3124
2917
|
lastUsed: Date.now(),
|
|
3125
2918
|
dailyLimit: this.config.nonMemberDailyLimit,
|
|
3126
2919
|
points: 0,
|
|
2920
|
+
pointsRefreshAt: 0,
|
|
2921
|
+
pointsLastRefreshedAt: 0,
|
|
3127
2922
|
nai5DailyUsage: 0,
|
|
3128
2923
|
nai5Bucket: 0,
|
|
3129
2924
|
nai5BucketDay: 0,
|
|
@@ -3168,14 +2963,15 @@ var MembershipSystem = class {
|
|
|
3168
2963
|
const bucketDay = user.nai5BucketDay || 0;
|
|
3169
2964
|
if (!bucketDay) {
|
|
3170
2965
|
const seedLimit = activeTier > 0 ? this.getTierBenefit(activeTier).nai5DailyLimit : 0;
|
|
3171
|
-
|
|
2966
|
+
const seed = Math.max(0, seedLimit - (user.nai5DailyUsage || 0));
|
|
2967
|
+
user.nai5Bucket = seedLimit > 0 ? Math.round(seed * 10) / 10 : 0;
|
|
3172
2968
|
user.nai5BucketDay = todayStart;
|
|
3173
2969
|
user.nai5DepositTier = activeTier;
|
|
3174
2970
|
} else if (bucketDay < todayStart && activeTier > 0) {
|
|
3175
2971
|
const limit = this.getTierBenefit(activeTier).nai5DailyLimit;
|
|
3176
2972
|
if (limit > 0) {
|
|
3177
2973
|
const daysMissed = Math.max(1, Math.min(7, Math.round((todayStart - bucketDay) / 864e5)));
|
|
3178
|
-
user.nai5Bucket = Math.min(limit * 7, (user.nai5Bucket || 0) + limit * daysMissed);
|
|
2974
|
+
user.nai5Bucket = Math.round(Math.min(limit * 7, (user.nai5Bucket || 0) + limit * daysMissed) * 10) / 10;
|
|
3179
2975
|
user.nai5BucketDay = todayStart;
|
|
3180
2976
|
user.nai5DepositTier = activeTier;
|
|
3181
2977
|
}
|
|
@@ -3241,6 +3037,17 @@ var MembershipSystem = class {
|
|
|
3241
3037
|
isNai5Model(model) {
|
|
3242
3038
|
return isNovelAIV5Model(modelMap[model] || model);
|
|
3243
3039
|
}
|
|
3040
|
+
/** 是否为 V5 Medium(Effort=Medium)模型。 */
|
|
3041
|
+
isNai5MediumModel(model) {
|
|
3042
|
+
return isNovelAIV5MediumModel(modelMap[model] || model);
|
|
3043
|
+
}
|
|
3044
|
+
/**
|
|
3045
|
+
* 该模型每次画图消耗的 nai5 免费次数权重。
|
|
3046
|
+
* Medium 档一次只扣 0.6 次,其余模型为 1 次。
|
|
3047
|
+
*/
|
|
3048
|
+
getNai5UsageWeight(model) {
|
|
3049
|
+
return this.isNai5MediumModel(model) ? 0.6 : 1;
|
|
3050
|
+
}
|
|
3244
3051
|
/**
|
|
3245
3052
|
* 获取用户生效等级的 nai5 / nai5c 每日免费限额。
|
|
3246
3053
|
* - 传入 userId:Lv1 = 配置字段 memberNai5DailyLimit,Lv2-Lv5 = tierBenefits;非会员返回 0
|
|
@@ -3348,24 +3155,28 @@ var MembershipSystem = class {
|
|
|
3348
3155
|
}
|
|
3349
3156
|
/**
|
|
3350
3157
|
* 本次 nai5 任务中,超出会员免费额度、需要按 Anlas 计费的张数。
|
|
3158
|
+
* - Medium 模型每次画图消耗 0.6 次免费额度(weight < 1)
|
|
3351
3159
|
* - 周桶模式:可用 = 桶余额 − 并发预占,超出可用部分计费(点数支付的部分不扣桶)
|
|
3352
3160
|
* - 旧模式:日限按用户生效等级取值;日限为 0 表示不额外限制,全部走 Opus 免费档(标准分辨率不扣点)
|
|
3353
3161
|
*/
|
|
3354
|
-
getNai5OverageCount(userId, drawCount = 1) {
|
|
3162
|
+
getNai5OverageCount(userId, drawCount = 1, model) {
|
|
3355
3163
|
if (!this.config.membershipEnabled) return 0;
|
|
3356
3164
|
const limit = this.getNai5DailyLimit(userId);
|
|
3357
3165
|
if (limit <= 0 || drawCount <= 0) return 0;
|
|
3358
3166
|
if (!this.isActiveMember(userId)) return 0;
|
|
3167
|
+
const weight = this.getNai5UsageWeight(model);
|
|
3168
|
+
const freeCountOf = /* @__PURE__ */ __name((available) => Math.min(drawCount, Math.floor(available / weight + 1e-9)), "freeCountOf");
|
|
3359
3169
|
if (this.isNai5BucketEnabled()) {
|
|
3360
3170
|
const bucket = this.userData[userId]?.nai5Bucket || 0;
|
|
3361
3171
|
const available = Math.max(0, bucket - (this.pendingNai5Usage[userId] || 0));
|
|
3362
|
-
return
|
|
3172
|
+
return drawCount - freeCountOf(available);
|
|
3363
3173
|
}
|
|
3364
3174
|
const used = this.getNai5DailyUsage(userId) + (this.pendingNai5Usage[userId] || 0);
|
|
3365
|
-
|
|
3175
|
+
const remaining = Math.max(0, limit - used);
|
|
3176
|
+
return drawCount - freeCountOf(remaining);
|
|
3366
3177
|
}
|
|
3367
3178
|
shouldChargeNai5Overage(userId, model, drawCount = 1) {
|
|
3368
|
-
return this.isNai5Model(model) && this.getNai5OverageCount(userId, drawCount) > 0;
|
|
3179
|
+
return this.isNai5Model(model) && this.getNai5OverageCount(userId, drawCount, model) > 0;
|
|
3369
3180
|
}
|
|
3370
3181
|
reserveNai5Usage(userId, count) {
|
|
3371
3182
|
if (!this.config.membershipEnabled || count <= 0) return;
|
|
@@ -3387,10 +3198,13 @@ var MembershipSystem = class {
|
|
|
3387
3198
|
this.userData[userId].dailyUsage += drawCount;
|
|
3388
3199
|
this.userData[userId].lastUsed = now;
|
|
3389
3200
|
if (this.isNai5Model(model)) {
|
|
3390
|
-
const
|
|
3391
|
-
|
|
3201
|
+
const weight = this.getNai5UsageWeight(model);
|
|
3202
|
+
const add = Math.round((nai5Count ?? drawCount) * weight * 10) / 10;
|
|
3203
|
+
const usage2 = Math.round(((this.userData[userId].nai5DailyUsage || 0) + add) * 10) / 10;
|
|
3204
|
+
this.userData[userId].nai5DailyUsage = usage2;
|
|
3392
3205
|
if (this.isNai5BucketEnabled()) {
|
|
3393
|
-
|
|
3206
|
+
const balance = Math.max(0, Math.round(((this.userData[userId].nai5Bucket || 0) - add) * 10) / 10);
|
|
3207
|
+
this.userData[userId].nai5Bucket = balance;
|
|
3394
3208
|
}
|
|
3395
3209
|
this.releaseNai5Usage(userId, add);
|
|
3396
3210
|
}
|
|
@@ -4010,7 +3824,7 @@ async function generateImageInner(runtime, session, options2, input) {
|
|
|
4010
3824
|
caption: { base_caption: base, char_captions: [] },
|
|
4011
3825
|
...!isNegative && { use_coords: false, use_order: true }
|
|
4012
3826
|
}), "createPrompt");
|
|
4013
|
-
const isNAI5 = model === "nai-diffusion-5-curated" || model === "nai-diffusion-5-full";
|
|
3827
|
+
const isNAI5 = model === "nai-diffusion-5-curated" || model === "nai-diffusion-5-full" || model === "nai-diffusion-5-full-medium";
|
|
4014
3828
|
parameters.params_version = isNAI5 ? 4 : 3;
|
|
4015
3829
|
parameters.sampler = sampler.sd2nai(options2.sampler, model);
|
|
4016
3830
|
if (parameters.uc) {
|
|
@@ -4023,6 +3837,12 @@ async function generateImageInner(runtime, session, options2, input) {
|
|
|
4023
3837
|
parameters.add_original_image = false;
|
|
4024
3838
|
parameters.legacy = false;
|
|
4025
3839
|
parameters.cfg_rescale = options2.rescale ?? session.resolve(runtime.config.rescale);
|
|
3840
|
+
if (model === "nai-diffusion-5-full-medium") {
|
|
3841
|
+
parameters.steps = 14;
|
|
3842
|
+
parameters.sampler = "k_euler_ancestral";
|
|
3843
|
+
parameters.cfg_rescale = 0;
|
|
3844
|
+
parameters.ucPreset = 2;
|
|
3845
|
+
}
|
|
4026
3846
|
const isNAI3 = model === "nai-diffusion-3";
|
|
4027
3847
|
const isNAI4 = model === "nai-diffusion-4-curated-preview" || model === "nai-diffusion-4-full" || model === "nai-diffusion-4-5-curated" || model === "nai-diffusion-4-5-full" || isNAI5;
|
|
4028
3848
|
if (isNAI3) {
|
|
@@ -4495,11 +4315,18 @@ async function generateImageInner(runtime, session, options2, input) {
|
|
|
4495
4315
|
if (!auditResult.pass) {
|
|
4496
4316
|
runtime.ctx.logger.warn(`[图片审核] 审核未通过: ${auditResult.message}, 分数: ${auditResult.score}`);
|
|
4497
4317
|
if (runtime.config.membershipEnabled) {
|
|
4318
|
+
const model2 = options2.model || runtime.config.model;
|
|
4498
4319
|
const nai5Count = options2.batch || 1;
|
|
4499
|
-
|
|
4320
|
+
const freeQuota = options2._nai5FreeCount;
|
|
4321
|
+
const freeCount = typeof freeQuota === "number" ? Math.min(freeQuota, nai5Count) : nai5Count;
|
|
4322
|
+
runtime.membershipSystem.incrementUsage(session.userId, 1, model2, freeCount);
|
|
4500
4323
|
if (options2._reservedNai5) {
|
|
4324
|
+
const weight = runtime.membershipSystem.getNai5UsageWeight(model2);
|
|
4325
|
+
options2._reservedNai5 = Math.max(0, options2._reservedNai5 - Math.round(freeCount * weight * 10) / 10);
|
|
4326
|
+
}
|
|
4327
|
+
if (typeof freeQuota === "number") {
|
|
4501
4328
|
;
|
|
4502
|
-
options2.
|
|
4329
|
+
options2._nai5FreeCount = Math.max(0, freeQuota - freeCount);
|
|
4503
4330
|
}
|
|
4504
4331
|
}
|
|
4505
4332
|
if (runtime.config.muteOnReviewFailed && session.guildId && session.userId) {
|
|
@@ -4569,11 +4396,18 @@ async function generateImageInner(runtime, session, options2, input) {
|
|
|
4569
4396
|
runtime.ctx.logger.debug(`${session.uid}: ${finalPrompt}`);
|
|
4570
4397
|
const messageIds = await session.send(getContent());
|
|
4571
4398
|
if (runtime.config.membershipEnabled) {
|
|
4399
|
+
const model2 = options2.model || runtime.config.model;
|
|
4572
4400
|
const nai5Count = options2.batch || 1;
|
|
4573
|
-
|
|
4401
|
+
const freeQuota = options2._nai5FreeCount;
|
|
4402
|
+
const freeCount = typeof freeQuota === "number" ? Math.min(freeQuota, nai5Count) : nai5Count;
|
|
4403
|
+
runtime.membershipSystem.incrementUsage(session.userId, 1, model2, freeCount);
|
|
4574
4404
|
if (options2._reservedNai5) {
|
|
4405
|
+
const weight = runtime.membershipSystem.getNai5UsageWeight(model2);
|
|
4406
|
+
options2._reservedNai5 = Math.max(0, options2._reservedNai5 - Math.round(freeCount * weight * 10) / 10);
|
|
4407
|
+
}
|
|
4408
|
+
if (typeof freeQuota === "number") {
|
|
4575
4409
|
;
|
|
4576
|
-
options2.
|
|
4410
|
+
options2._nai5FreeCount = Math.max(0, freeQuota - freeCount);
|
|
4577
4411
|
}
|
|
4578
4412
|
}
|
|
4579
4413
|
if (messageIds.length && runtime.config.recallTimeout) {
|
|
@@ -4637,7 +4471,7 @@ function calculateTaskPointsCost(runtime, session, options2, width, height, isIm
|
|
|
4637
4471
|
for (let i = 0; i < count; i++) perImage.push(unitFree);
|
|
4638
4472
|
return { total: unitFree * count, perImage };
|
|
4639
4473
|
}
|
|
4640
|
-
const overageCount = runtime.membershipSystem.getNai5OverageCount(uid, count);
|
|
4474
|
+
const overageCount = runtime.membershipSystem.getNai5OverageCount(uid, count, resolvedModel);
|
|
4641
4475
|
const freeCount = count - overageCount;
|
|
4642
4476
|
const unitOverage = overageCount > 0 ? getUnitPointsCost(runtime, session, options2, width, height, isImg2Img, preciseRefCount, true, resolvedModel) : unitFree;
|
|
4643
4477
|
for (let i = 0; i < freeCount; i++) perImage.push(unitFree);
|
|
@@ -4943,7 +4777,7 @@ function registerNovelai(ctx, config, runtime) {
|
|
|
4943
4777
|
"$1-$2"
|
|
4944
4778
|
);
|
|
4945
4779
|
});
|
|
4946
|
-
const cmd = ctx.command("novelai [prompts...]").alias("nai").alias("imagine").alias("nai4", { options: { model: "nai-v4-full" } }).alias("nai4c", { options: { model: "nai-v4-curated-preview" } }).alias("nai4-5c", { options: { model: "nai-v4-5-curated" } }).alias("nai4-5", { options: { model: "nai-v4-5-full" } }).alias("nai5c", { options: { model: "nai-v5-curated" } }).alias("nai5", { options: { model: "nai-v5-full" } }).userFields(["authority"]).shortcut("imagine", { i18n: true, fuzzy: true }).shortcut("enhance", { i18n: true, fuzzy: true, options: { enhance: true } }).option("enhance", "-e", { hidden: some(thirdParty, noImage) }).option("model", "-m <model>", { type: models, hidden: thirdParty }).option("resolution", "-r <resolution>", { type: resolution2 }).option("output", "-o", { type: ["minimal", "default", "verbose"] }).option("override", "-O").option("sampler", "-s <sampler>").option("seed", "-x <seed:number>").option("steps", "-t <step>", { type: step }).option("scale", "-c <scale:number>").option("rescale", "-R <rescale:number>").option("noise", "-n <noise:number>", { hidden: thirdParty }).option("strength", "-N <strength:number>").option("variety", "-v").option("hiresFix", "-H", { hidden: /* @__PURE__ */ __name(() => config.type !== "sd-webui", "hidden") }).option("hiresFixSteps", "<step>", { type: step, hidden: /* @__PURE__ */ __name(() => config.type !== "sd-webui", "hidden") }).option("smea", "-S", { hidden: /* @__PURE__ */ __name(() => config.model !== "nai-v3", "hidden") }).option("smeaDyn", "-d", { hidden: /* @__PURE__ */ __name(() => config.model !== "nai-v3", "hidden") }).option("scheduler", "-C <scheduler:string>", {
|
|
4780
|
+
const cmd = ctx.command("novelai [prompts...]").alias("nai").alias("imagine").alias("nai4", { options: { model: "nai-v4-full" } }).alias("nai4c", { options: { model: "nai-v4-curated-preview" } }).alias("nai4-5c", { options: { model: "nai-v4-5-curated" } }).alias("nai4-5", { options: { model: "nai-v4-5-full" } }).alias("nai5c", { options: { model: "nai-v5-curated" } }).alias("nai5", { options: { model: "nai-v5-full" } }).alias("nai5m", { options: { model: "nai-v5-full-medium" } }).userFields(["authority"]).shortcut("imagine", { i18n: true, fuzzy: true }).shortcut("enhance", { i18n: true, fuzzy: true, options: { enhance: true } }).option("enhance", "-e", { hidden: some(thirdParty, noImage) }).option("model", "-m <model>", { type: models, hidden: thirdParty }).option("resolution", "-r <resolution>", { type: resolution2 }).option("output", "-o", { type: ["minimal", "default", "verbose"] }).option("override", "-O").option("sampler", "-s <sampler>").option("seed", "-x <seed:number>").option("steps", "-t <step>", { type: step }).option("scale", "-c <scale:number>").option("rescale", "-R <rescale:number>").option("noise", "-n <noise:number>", { hidden: thirdParty }).option("strength", "-N <strength:number>").option("variety", "-v").option("hiresFix", "-H", { hidden: /* @__PURE__ */ __name(() => config.type !== "sd-webui", "hidden") }).option("hiresFixSteps", "<step>", { type: step, hidden: /* @__PURE__ */ __name(() => config.type !== "sd-webui", "hidden") }).option("smea", "-S", { hidden: /* @__PURE__ */ __name(() => config.model !== "nai-v3", "hidden") }).option("smeaDyn", "-d", { hidden: /* @__PURE__ */ __name(() => config.model !== "nai-v3", "hidden") }).option("scheduler", "-C <scheduler:string>", {
|
|
4947
4781
|
hidden: /* @__PURE__ */ __name(() => config.type === "naifu", "hidden"),
|
|
4948
4782
|
type: ["token", "login"].includes(config.type) ? scheduler.nai : config.type === "sd-webui" ? scheduler.sd : config.type === "stable-horde" ? scheduler.horde : []
|
|
4949
4783
|
}).option("decrisper", "-D", { hidden: thirdParty }).option("undesired", "-u <undesired:text>").option("noTranslator", "-T", { hidden: /* @__PURE__ */ __name(() => !ctx.translator || !config.translator, "hidden") }).option("iterations", "-i <iterations:posint>", { fallback: 1, hidden: /* @__PURE__ */ __name(() => config.maxIterations <= 1, "hidden") }).option("batch", "-b <batch:option>", { fallback: 1, hidden: /* @__PURE__ */ __name(() => config.maxIterations <= 1, "hidden") }).option("chars", "-K <chars>").option("inpaint", "-M", { hidden: thirdParty }).option("preciseRef", "-P", { hidden: thirdParty }).option("preciseRefParams", "-p <params:string>").action(async ({ session, options: options2, name: name2 }, ...prompts) => {
|
|
@@ -5026,6 +4860,10 @@ function registerNovelai(ctx, config, runtime) {
|
|
|
5026
4860
|
const preciseRefCount = options2?._preciseRefImages?.length || 0;
|
|
5027
4861
|
const drawCount = getTaskDrawCount(options2);
|
|
5028
4862
|
const isImg2Img = !!options2.inpaint || imgUrl_check;
|
|
4863
|
+
const resolvedModel = options2.model || config.model;
|
|
4864
|
+
if (membershipSystem.isNai5MediumModel(resolvedModel)) {
|
|
4865
|
+
options2.steps = 14;
|
|
4866
|
+
}
|
|
5029
4867
|
const cost = calculateTaskPointsCost(
|
|
5030
4868
|
runtime,
|
|
5031
4869
|
session,
|
|
@@ -5035,14 +4873,18 @@ function registerNovelai(ctx, config, runtime) {
|
|
|
5035
4873
|
isImg2Img,
|
|
5036
4874
|
preciseRefCount,
|
|
5037
4875
|
userId,
|
|
5038
|
-
|
|
4876
|
+
resolvedModel,
|
|
5039
4877
|
drawCount
|
|
5040
4878
|
);
|
|
5041
4879
|
pointsCost = cost.total;
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
4880
|
+
const nai5OverageCount = membershipSystem.isNai5Model(resolvedModel) ? membershipSystem.getNai5OverageCount(userId, drawCount, resolvedModel) : 0;
|
|
4881
|
+
nai5Overage = nai5OverageCount > 0;
|
|
4882
|
+
if (membershipSystem.isNai5Model(resolvedModel) && membershipSystem.getNai5DailyLimit(userId) > 0) {
|
|
4883
|
+
const weight = membershipSystem.getNai5UsageWeight(resolvedModel);
|
|
4884
|
+
const reserveCount = Math.round(weight * drawCount * 10) / 10;
|
|
4885
|
+
membershipSystem.reserveNai5Usage(userId, reserveCount);
|
|
4886
|
+
options2._reservedNai5 = reserveCount;
|
|
4887
|
+
options2._nai5FreeCount = drawCount - nai5OverageCount;
|
|
5046
4888
|
}
|
|
5047
4889
|
if (pointsCost > 0) {
|
|
5048
4890
|
const result = await membershipSystem.deductPoints(userId, pointsCost);
|
|
@@ -5210,6 +5052,7 @@ function registerRedraw(ctx, config, runtime) {
|
|
|
5210
5052
|
}
|
|
5211
5053
|
let redrawDeductedPoints = 0;
|
|
5212
5054
|
let redrawPerTask = [];
|
|
5055
|
+
let redrawFreePerTask = [];
|
|
5213
5056
|
let nai5Overage = false;
|
|
5214
5057
|
if (config.membershipEnabled && config.pointsEnabled) {
|
|
5215
5058
|
let resWidth = 832, resHeight = 1216;
|
|
@@ -5246,18 +5089,27 @@ function registerRedraw(ctx, config, runtime) {
|
|
|
5246
5089
|
drawCount
|
|
5247
5090
|
);
|
|
5248
5091
|
const imagesPerTask = Math.max(1, Math.floor(drawCount / repeatCount));
|
|
5092
|
+
const redrawModel = lastOptions.model || config.model;
|
|
5093
|
+
const nai5OverageCount = membershipSystem.isNai5Model(redrawModel) ? membershipSystem.getNai5OverageCount(userId, drawCount, redrawModel) : 0;
|
|
5094
|
+
const nai5FreeTotal = drawCount - nai5OverageCount;
|
|
5095
|
+
redrawFreePerTask.length = 0;
|
|
5249
5096
|
for (let i = 0; i < repeatCount; i++) {
|
|
5250
5097
|
const start = i * imagesPerTask;
|
|
5251
|
-
|
|
5098
|
+
const end = Math.min(drawCount, start + imagesPerTask);
|
|
5099
|
+
redrawPerTask.push(cost.perImage.slice(start, end).reduce((sum, n) => sum + n, 0));
|
|
5100
|
+
redrawFreePerTask.push(Math.max(0, Math.min(end, nai5FreeTotal) - start));
|
|
5252
5101
|
}
|
|
5253
|
-
nai5Overage =
|
|
5254
|
-
|
|
5255
|
-
|
|
5102
|
+
nai5Overage = nai5OverageCount > 0;
|
|
5103
|
+
let nai5ReserveCount = drawCount;
|
|
5104
|
+
if (membershipSystem.isNai5Model(redrawModel) && membershipSystem.getNai5DailyLimit(userId) > 0) {
|
|
5105
|
+
const weight = membershipSystem.getNai5UsageWeight(redrawModel);
|
|
5106
|
+
nai5ReserveCount = Math.round(weight * drawCount * 10) / 10;
|
|
5107
|
+
membershipSystem.reserveNai5Usage(userId, nai5ReserveCount);
|
|
5256
5108
|
}
|
|
5257
5109
|
if (cost.total > 0) {
|
|
5258
5110
|
const result = await membershipSystem.deductPoints(userId, cost.total);
|
|
5259
5111
|
if (result === -1) {
|
|
5260
|
-
membershipSystem.releaseNai5Usage(userId,
|
|
5112
|
+
membershipSystem.releaseNai5Usage(userId, nai5ReserveCount);
|
|
5261
5113
|
const currentPoints = membershipSystem.getPoints(userId);
|
|
5262
5114
|
queueSystem.releaseRedrawLock();
|
|
5263
5115
|
return session.text("commands.novelai.messages.points-insufficient", [currentPoints, cost.total]);
|
|
@@ -5331,7 +5183,10 @@ function registerRedraw(ctx, config, runtime) {
|
|
|
5331
5183
|
if (unitCost > 0) taskOptions._deductedPoints = unitCost;
|
|
5332
5184
|
}
|
|
5333
5185
|
if (config.membershipEnabled && membershipSystem.isNai5Model(lastTask.options?.model || config.model) && membershipSystem.getNai5DailyLimit(currentUserId) > 0) {
|
|
5334
|
-
|
|
5186
|
+
const weight = membershipSystem.getNai5UsageWeight(lastTask.options?.model || config.model);
|
|
5187
|
+
const perTaskCount = getTaskDrawCount(lastTask.options, 1);
|
|
5188
|
+
taskOptions._reservedNai5 = Math.round(weight * perTaskCount * 10) / 10;
|
|
5189
|
+
taskOptions._nai5FreeCount = redrawFreePerTask[index] ?? perTaskCount;
|
|
5335
5190
|
}
|
|
5336
5191
|
queueSystem.taskQueue.push({
|
|
5337
5192
|
session: taskSession,
|
|
@@ -5482,6 +5337,7 @@ function registerMember(ctx, config, runtime) {
|
|
|
5482
5337
|
return "您没有权限设置会员状态";
|
|
5483
5338
|
}
|
|
5484
5339
|
membershipSystem.checkAndResetDailyUsage(targetId);
|
|
5340
|
+
await membershipSystem.ensurePointsFresh(targetId);
|
|
5485
5341
|
if (options2.refreshPoints) {
|
|
5486
5342
|
if (!config.pointsEnabled) {
|
|
5487
5343
|
return "点数控制未启用";
|
|
@@ -5609,10 +5465,12 @@ nai5/nai5c 今日免费:${benefit.nai5DailyLimit} 次,已用 ${nai5Used} 次
|
|
|
5609
5465
|
pointsInfo = `
|
|
5610
5466
|
点数余额:${points}`;
|
|
5611
5467
|
if (config.pointsMode === "periodic") {
|
|
5612
|
-
const
|
|
5613
|
-
|
|
5468
|
+
const refreshAt = membershipSystem.getNextPointsRefreshAt(targetId);
|
|
5469
|
+
const remainDaysRefresh = membershipSystem.getDaysUntilNextRefresh(targetId);
|
|
5470
|
+
if (refreshAt > 0) {
|
|
5471
|
+
const refreshDate = new Date(refreshAt).toLocaleString();
|
|
5614
5472
|
pointsInfo += `
|
|
5615
|
-
下次点数刷新时间:${
|
|
5473
|
+
下次点数刷新时间:${refreshDate}(约${remainDaysRefresh}天后,Lv${activeTier} 档刷新为 ${benefit.pointsRefresh} 点)`;
|
|
5616
5474
|
}
|
|
5617
5475
|
}
|
|
5618
5476
|
}
|
|
@@ -5625,10 +5483,11 @@ nai5/nai5c 今日免费:${benefit.nai5DailyLimit} 次,已用 ${nai5Used} 次
|
|
|
5625
5483
|
pointsInfo = `
|
|
5626
5484
|
点数余额:${user.points || 0}`;
|
|
5627
5485
|
if (config.pointsMode === "periodic" && config.pointsRefreshIncludeNonMember) {
|
|
5628
|
-
const
|
|
5629
|
-
if (
|
|
5486
|
+
const refreshAt = membershipSystem.getNextPointsRefreshAt(targetId);
|
|
5487
|
+
if (refreshAt > 0) {
|
|
5488
|
+
const refreshDate = new Date(refreshAt).toLocaleString();
|
|
5630
5489
|
pointsInfo += `
|
|
5631
|
-
下次点数刷新时间:${membershipSystem.
|
|
5490
|
+
下次点数刷新时间:${refreshDate}(约${membershipSystem.getDaysUntilNextRefresh(targetId)}天后)`;
|
|
5632
5491
|
}
|
|
5633
5492
|
}
|
|
5634
5493
|
}
|
|
@@ -5647,7 +5506,7 @@ nai5/nai5c 今日免费:${benefit.nai5DailyLimit} 次,已用 ${nai5Used} 次
|
|
|
5647
5506
|
}
|
|
5648
5507
|
});
|
|
5649
5508
|
if (config.memberDebugCommandEnabled) {
|
|
5650
|
-
ctx.command("novelai.member-debug", "会员系统调试指令", { authority: config.memberDebugCommandAuthLv }).alias("会员调试").option("cleanup", "-c 立即执行会员信息清理").option("remind", "-r 立即执行会员到期提醒").option("status", "-s 查看定时任务状态").option("resetUsage", "-u <userId:string> 重置指定用户的使用次数").option("addDaysAll", "-a <days:number> 给会员增加天数(配合 -t 仅操作指定等级)").option("tier", "-t <tier:number> 指定生效等级(Lv1-Lv5),缺省对所有会员操作").option("refreshPoints", "-f
|
|
5509
|
+
ctx.command("novelai.member-debug", "会员系统调试指令", { authority: config.memberDebugCommandAuthLv }).alias("会员调试").option("cleanup", "-c 立即执行会员信息清理").option("remind", "-r 立即执行会员到期提醒").option("status", "-s 查看定时任务状态").option("resetUsage", "-u <userId:string> 重置指定用户的使用次数").option("addDaysAll", "-a <days:number> 给会员增加天数(配合 -t 仅操作指定等级)").option("tier", "-t <tier:number> 指定生效等级(Lv1-Lv5),缺省对所有会员操作").option("refreshPoints", "-f 立即手动刷新点数(不改变自动刷新计划,配合 -t 仅刷新指定等级)").option("addPoints", "--add-points <amount:number> 给会员加点数(配合 -t 仅操作指定等级)").option("subPoints", "--sub-points <amount:number> 给会员减点数(配合 -t 仅操作指定等级)").option("setPoints", "--set-points <value:string> 设置指定用户点数,格式: 用户ID:点数").option("addBucket", "--add-bucket [days:number] 给会员赠送免费次数(发福利:每人按各自档位每日额度 × 天数到账,受 7 天上限约束;配合 -t 指定等级、--bucket-user 指定单人)").option("bucketUser", "--bucket-user <userId:string> 配合 --add-bucket 仅给指定用户赠送").option("setBucket", "--set-bucket <value:string> 设置指定用户免费次数余额,格式: 用户ID:次数").option("importData", "--import 从 JSON 文件导入数据").action(async ({ session, options: options2 }) => {
|
|
5651
5510
|
if (!config.membershipEnabled) {
|
|
5652
5511
|
return "会员系统未启用";
|
|
5653
5512
|
}
|
|
@@ -5819,24 +5678,16 @@ nai5 已使用:${user.nai5DailyUsage || 0} 次` : "";
|
|
|
5819
5678
|
`;
|
|
5820
5679
|
statusMsg += `✅ 点数控制:已启用
|
|
5821
5680
|
`;
|
|
5822
|
-
statusMsg += ` 点数模式:${config.pointsMode === "periodic" ? "
|
|
5681
|
+
statusMsg += ` 点数模式:${config.pointsMode === "periodic" ? "按周期惰性刷新" : "永久点数"}
|
|
5823
5682
|
`;
|
|
5824
5683
|
if (config.pointsMode === "periodic") {
|
|
5825
5684
|
statusMsg += ` 刷新周期:${config.pointsRefreshCycleDays || 30} 天
|
|
5826
|
-
`;
|
|
5827
|
-
statusMsg += ` 刷新检查时刻:${config.pointsRefreshCheckTime || "00:05"}
|
|
5828
5685
|
`;
|
|
5829
5686
|
statusMsg += ` 刷新点数:${config.pointsRefreshAmount || 200}
|
|
5830
5687
|
`;
|
|
5831
|
-
statusMsg += `
|
|
5832
|
-
`;
|
|
5833
|
-
statusMsg += ` 周期锚点:${membershipSystem.formatRefreshAnchor()}
|
|
5688
|
+
statusMsg += ` 刷新触发:用户发生点数相关操作时按用户惰性执行
|
|
5834
5689
|
`;
|
|
5835
|
-
statusMsg += `
|
|
5836
|
-
`;
|
|
5837
|
-
const nextDays = membershipSystem.getDaysUntilNextRefresh();
|
|
5838
|
-
const planLabel = membershipSystem.formatRefreshPlanTime();
|
|
5839
|
-
statusMsg += ` 下次刷新:${membershipSystem.formatNextRefreshTime()}(${nextDays}天后,计划 ${planLabel})${membershipSystem.isPointsRefreshOverdue() ? " ⚠️ 已逾期,将在下个检查点补刷" : ""}
|
|
5690
|
+
statusMsg += ` 刷新范围:${config.pointsRefreshIncludeNonMember ? "所有用户" : "仅会员"}
|
|
5840
5691
|
`;
|
|
5841
5692
|
}
|
|
5842
5693
|
statusMsg += ` 默认点数:${config.pointsDefault || 200}
|
|
@@ -5867,27 +5718,6 @@ nai5 已使用:${user.nai5DailyUsage || 0} 次` : "";
|
|
|
5867
5718
|
await membershipSystem.checkAndRemindExpiringMembers();
|
|
5868
5719
|
return "✅ 提醒完成!请查看控制台日志获取详细信息。";
|
|
5869
5720
|
}
|
|
5870
|
-
if (options2.setAnchor !== void 0) {
|
|
5871
|
-
if (!config.pointsEnabled || config.pointsMode !== "periodic") {
|
|
5872
|
-
return "❌ 未启用周期刷新,无法设置周期锚点";
|
|
5873
|
-
}
|
|
5874
|
-
const result = await membershipSystem.setPointsRefreshAnchor(options2.setAnchor);
|
|
5875
|
-
return result;
|
|
5876
|
-
}
|
|
5877
|
-
if (options2.setNextAt !== void 0) {
|
|
5878
|
-
if (!config.pointsEnabled || config.pointsMode !== "periodic") {
|
|
5879
|
-
return "❌ 未启用周期刷新,无法设置下次刷新时刻";
|
|
5880
|
-
}
|
|
5881
|
-
const result = await membershipSystem.setPointsRefreshNextAt(options2.setNextAt);
|
|
5882
|
-
return result;
|
|
5883
|
-
}
|
|
5884
|
-
if (options2.resetCycle) {
|
|
5885
|
-
if (!config.pointsEnabled || config.pointsMode !== "periodic") {
|
|
5886
|
-
return "❌ 未启用周期刷新,无法重置周期";
|
|
5887
|
-
}
|
|
5888
|
-
const result = await membershipSystem.resetPointsRefreshCycle();
|
|
5889
|
-
return result;
|
|
5890
|
-
}
|
|
5891
5721
|
if (options2.refreshPoints) {
|
|
5892
5722
|
if (!config.pointsEnabled) {
|
|
5893
5723
|
return "❌ 点数控制未启用";
|
|
@@ -5897,12 +5727,8 @@ nai5 已使用:${user.nai5DailyUsage || 0} 次` : "";
|
|
|
5897
5727
|
}
|
|
5898
5728
|
const refreshScope = debugTierFilter !== void 0 ? `Lv${debugTierFilter} 会员` : "所有会员";
|
|
5899
5729
|
await session.send(`正在为${refreshScope}执行点数刷新...`);
|
|
5900
|
-
const overdueBefore = membershipSystem.isPointsRefreshOverdue();
|
|
5901
5730
|
const result = await membershipSystem.refreshPoints(debugTierFilter);
|
|
5902
|
-
|
|
5903
|
-
ℹ️ 本次刷新不改变周期锚点(${membershipSystem.formatRefreshAnchor()}),下次刷新仍为 ${membershipSystem.formatNextRefreshTime()};如需立即重开周期请用 --reset-cycle` : `
|
|
5904
|
-
ℹ️ 周期不受影响,下次刷新仍为 ${membershipSystem.formatNextRefreshTime()}(${membershipSystem.getDaysUntilNextRefresh()}天后)`;
|
|
5905
|
-
return result.message + note;
|
|
5731
|
+
return result.message;
|
|
5906
5732
|
}
|
|
5907
5733
|
if (options2.addPoints !== void 0) {
|
|
5908
5734
|
if (!config.pointsEnabled) {
|
|
@@ -6744,6 +6570,7 @@ var usage = `
|
|
|
6744
6570
|
| \`nai4-5\` | \`nai-diffusion-4-5-full\` | V4.5 全量模型 |
|
|
6745
6571
|
| \`nai4-5c\` | \`nai-diffusion-4-5-curated\` | V4.5 精选模型 |
|
|
6746
6572
|
| \`nai5\` | \`nai-diffusion-5-full\` | V5 全量模型 |
|
|
6573
|
+
| \`nai5m\` | \`nai-diffusion-5-full-medium\` | V5 Medium(Effort=Medium,固定 14 步,每次扣 0.6 次 nai5 额度) |
|
|
6747
6574
|
| \`nai5c\` | \`nai-diffusion-5-curated\` | V5 精选模型 |
|
|
6748
6575
|
|
|
6749
6576
|
### 🛠️ 功能列表
|
|
@@ -162,6 +162,13 @@ export declare class MembershipSystem {
|
|
|
162
162
|
isActiveMember(userId: string): boolean;
|
|
163
163
|
canUseNai5(userId: string, session: Session, model?: string): boolean | string;
|
|
164
164
|
isNai5Model(model?: string): boolean;
|
|
165
|
+
/** 是否为 V5 Medium(Effort=Medium)模型。 */
|
|
166
|
+
isNai5MediumModel(model?: string): boolean;
|
|
167
|
+
/**
|
|
168
|
+
* 该模型每次画图消耗的 nai5 免费次数权重。
|
|
169
|
+
* Medium 档一次只扣 0.6 次,其余模型为 1 次。
|
|
170
|
+
*/
|
|
171
|
+
getNai5UsageWeight(model?: string): number;
|
|
165
172
|
/**
|
|
166
173
|
* 获取用户生效等级的 nai5 / nai5c 每日免费限额。
|
|
167
174
|
* - 传入 userId:Lv1 = 配置字段 memberNai5DailyLimit,Lv2-Lv5 = tierBenefits;非会员返回 0
|
|
@@ -201,10 +208,11 @@ export declare class MembershipSystem {
|
|
|
201
208
|
}>;
|
|
202
209
|
/**
|
|
203
210
|
* 本次 nai5 任务中,超出会员免费额度、需要按 Anlas 计费的张数。
|
|
211
|
+
* - Medium 模型每次画图消耗 0.6 次免费额度(weight < 1)
|
|
204
212
|
* - 周桶模式:可用 = 桶余额 − 并发预占,超出可用部分计费(点数支付的部分不扣桶)
|
|
205
213
|
* - 旧模式:日限按用户生效等级取值;日限为 0 表示不额外限制,全部走 Opus 免费档(标准分辨率不扣点)
|
|
206
214
|
*/
|
|
207
|
-
getNai5OverageCount(userId: string, drawCount?: number): number;
|
|
215
|
+
getNai5OverageCount(userId: string, drawCount?: number, model?: string): number;
|
|
208
216
|
shouldChargeNai5Overage(userId: string, model?: string, drawCount?: number): boolean;
|
|
209
217
|
reserveNai5Usage(userId: string, count: number): void;
|
|
210
218
|
releaseNai5Usage(userId: string, count: number): void;
|
|
@@ -3,7 +3,9 @@ import { Config } from '../config';
|
|
|
3
3
|
import { Subscription } from '../types';
|
|
4
4
|
/** 获取 NovelAI 当前订阅信息(包含 Opus usage 状态)。 */
|
|
5
5
|
export declare function fetchSubscription(ctx: Context, config: Config, token: string): Promise<Subscription>;
|
|
6
|
-
/** NovelAI V5
|
|
6
|
+
/** NovelAI Diffusion V5 Medium(Effort=Medium,官网独立模型 ID)。 */
|
|
7
|
+
export declare function isNovelAIV5MediumModel(model: string): boolean;
|
|
8
|
+
/** NovelAI V5 的免费额度模型(含 Medium)。 */
|
|
7
9
|
export declare function isNovelAIV5Model(model: string): boolean;
|
|
8
10
|
/** 判断订阅响应中的 Opus 免费额度是否已经不可用。 */
|
|
9
11
|
export declare function isOpusQuotaExhausted(subscription: Subscription): boolean;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-hhs-huatu",
|
|
3
3
|
"description": "Generate images by diffusion models",
|
|
4
|
-
"version": "1.3.
|
|
4
|
+
"version": "1.3.15",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
|
@@ -47,11 +47,11 @@
|
|
|
47
47
|
]
|
|
48
48
|
},
|
|
49
49
|
"description": {
|
|
50
|
-
"en": "Image Generation. Based on [novelai-bot](https://bot.novelai.dev/) plugin, supports NovelAI v5, v4.5 full, characters feature, director tools, and inpainting.",
|
|
51
|
-
"zh": "对[novelai-bot](https://bot.novelai.dev/)插件二次开发,已经支持NovelAI v5
|
|
52
|
-
"zh-TW": "基於[novelai-bot](https://bot.novelai.dev/)插件二次開發,已支持NovelAI v5
|
|
53
|
-
"fr": "Génération d'images. Basé sur le plugin [novelai-bot](https://bot.novelai.dev/), prend en charge NovelAI v5, v4.5 full, la fonctionnalité characters, les outils de direction et l'inpainting.",
|
|
54
|
-
"ja": "画像生成。[novelai-bot](https://bot.novelai.dev/)プラグインの二次開発。NovelAI v5
|
|
50
|
+
"en": "Image Generation. Based on [novelai-bot](https://bot.novelai.dev/) plugin, supports NovelAI v5 (incl. Medium effort), v4.5 full, characters feature, director tools, and inpainting.",
|
|
51
|
+
"zh": "对[novelai-bot](https://bot.novelai.dev/)插件二次开发,已经支持NovelAI v5(含 Medium Effort)、v4.5full和characters功能,导演工具功能,局部重绘,精准参考,点数控制系统",
|
|
52
|
+
"zh-TW": "基於[novelai-bot](https://bot.novelai.dev/)插件二次開發,已支持NovelAI v5(含 Medium Effort)、v4.5 full和characters功能、導演工具、局部重繪、精準參考、點數控制系統",
|
|
53
|
+
"fr": "Génération d'images. Basé sur le plugin [novelai-bot](https://bot.novelai.dev/), prend en charge NovelAI v5 (dont l'effort Medium), v4.5 full, la fonctionnalité characters, les outils de direction et l'inpainting.",
|
|
54
|
+
"ja": "画像生成。[novelai-bot](https://bot.novelai.dev/)プラグインの二次開発。NovelAI v5(Medium Effort を含む)、v4.5 full、characters機能、ディレクターツール、部分再描画に対応。"
|
|
55
55
|
}
|
|
56
56
|
},
|
|
57
57
|
"keywords": [
|
package/readme.md
CHANGED
|
@@ -165,7 +165,7 @@ nai4-5 <提示词> -P -p "cs,1,1;c,0.8,0.9"
|
|
|
165
165
|
|
|
166
166
|
**计费细则:**
|
|
167
167
|
- **默认消耗**:`ceil(2.951823174884865e-6 × 像素 + 5.753298233447344e-7 × 像素 × 步数)`。
|
|
168
|
-
- **V5(nai5 / nai5c)**:在上述结果上再乘 **1.5**。例如 832×1216、28 步超额时,V4.5 为 20、V5 为 30
|
|
168
|
+
- **V5(nai5 / nai5m / nai5c)**:在上述结果上再乘 **1.5**。例如 832×1216、28 步超额时,V4.5 为 20、V5 为 30。`nai5m` 固定 14 步,按同一公式计算(标准竖图约 18 点)。
|
|
169
169
|
- **SMEA / DYN 加成**:SMEA 单独开启 **1.2 倍**,同时开启 SMEA 和 DYN **1.4 倍**。
|
|
170
170
|
- **以图生图**:按去噪强度(`strength`)缩放后向上取整,最少 2 点。
|
|
171
171
|
- **精确参考(-P)**:有参考图时取消 Opus 免费档,并每张参考图额外扣除 5 点数。
|
|
@@ -215,8 +215,8 @@ nai4-5 <提示词> -P -p "cs,1,1;c,0.8,0.9"
|
|
|
215
215
|
|
|
216
216
|
### ⚙️ 其他功能
|
|
217
217
|
|
|
218
|
-
- **多模型支持**:快捷指令 `nai4`、`nai4c`、`nai4-5`、`nai4-5c`、`nai5`、`nai5c` 直接调用不同模型
|
|
219
|
-
- **Opus 额度保护**:使用 `/配额` 查看 Opus 免费 V5 额度、下次恢复时间和状态;额度耗尽时默认阻止 `nai5`/`nai5c`,可在配置中开启允许消耗 Anlas 的熔断开关
|
|
218
|
+
- **多模型支持**:快捷指令 `nai4`、`nai4c`、`nai4-5`、`nai4-5c`、`nai5`、`nai5m`、`nai5c` 直接调用不同模型
|
|
219
|
+
- **Opus 额度保护**:使用 `/配额` 查看 Opus 免费 V5 额度、下次恢复时间和状态;额度耗尽时默认阻止 `nai5`/`nai5m`/`nai5c`,可在配置中开启允许消耗 Anlas 的熔断开关
|
|
220
220
|
- **参数调整**:`-R` 参数调整 cfg_rescale(范围 0-1)
|
|
221
221
|
- **调试模式**:可配置是否输出详细调试日志
|
|
222
222
|
|
|
@@ -232,6 +232,7 @@ nai4c <提示词> # 使用 v4 curated 模型
|
|
|
232
232
|
nai4-5 <提示词> # 使用 v4.5 full 模型
|
|
233
233
|
nai4-5c <提示词> # 使用 v4.5 curated 模型
|
|
234
234
|
nai5 <提示词> # 使用 v5 full 模型
|
|
235
|
+
nai5m <提示词> # 使用 v5 Medium 模型(固定 14 步,一次扣 0.6 次免费额度)
|
|
235
236
|
nai5c <提示词> # 使用 v5 curated 模型
|
|
236
237
|
/配额 # 查看 Opus 免费 V5 额度
|
|
237
238
|
|
|
@@ -366,6 +367,23 @@ Region 的值一般为 ap- 加上你选择的所属地域的拼音,以下为
|
|
|
366
367
|
|
|
367
368
|
## 更新日志
|
|
368
369
|
|
|
370
|
+
### v1.3.15
|
|
371
|
+
|
|
372
|
+
- ✨ **新增 `nai5m` 指令:NovelAI Diffusion V5 Medium(Effort=Medium)**
|
|
373
|
+
- 对应官网新增的 Effort 切换,本质是独立模型 ID `nai-diffusion-5-full-medium`(重绘为 `nai-diffusion-5-full-medium-inpainting`)
|
|
374
|
+
- 官网行为同步:固定 **14 步**、采样器固定 `k_euler_ancestral`、`cfg_rescale` 禁用(发送 0)、`ucPreset` 固定 heavy(发送 2);用户传入的 `-t` 步数将被忽略
|
|
375
|
+
- 与 `nai5` 能力一致:支持多角色提示词(V5 自由坐标定位)、走 V5 参数协议(`params_version: 4`)、受 Opus 免费额度熔断与会员限制约束
|
|
376
|
+
- **计费**:沿用 V5 官网公式,14 步自然比 High(28 步)便宜(标准竖图约 18 点 vs 30 点)
|
|
377
|
+
- ✨ **`nai5m` 免费额度按 0.6 权重扣减**
|
|
378
|
+
- 使用 `nai5m` 画一张只扣 **0.6 次** nai5 免费额度(日限/周桶均按权重折算),例如日限 20 次可画 33 张全免费的 Medium
|
|
379
|
+
- 免费额度不足以覆盖一张时(如剩 0.5 < 0.6),该张自动按点数计费,**剩余额度保留不清零**(点数支付的部分从不扣免费额度)
|
|
380
|
+
- 🐛 **修复点数支付后残余免费额度被误扣的问题**
|
|
381
|
+
- 修复前:出图后按全部张数扣免费额度,额度剩 0.x 时付了点数还会把残余额度一并抹掉
|
|
382
|
+
- 修复后:只有免费额度内的张数扣额度,点数支付的张数不扣(重画路径同步修复)
|
|
383
|
+
- 🐛 **修复免费额度浮点误差累积问题**
|
|
384
|
+
- 0.6 权重连加/连减会产生 `0.3999999999999999` 这类浮点尾巴,所有额度变动处统一对齐到 0.1
|
|
385
|
+
- 数据库字段 `nai5DailyUsage` / `nai5Bucket` 类型由 `unsigned` 改为 `float`,支持小数额度(控制台可正常编辑)
|
|
386
|
+
|
|
369
387
|
### v1.2.7
|
|
370
388
|
|
|
371
389
|
- ✨ **NAI 5 角色自由坐标定位**
|