koishi-plugin-cnb-bot 0.1.0
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/LICENSE +21 -0
- package/README.md +107 -0
- package/lib/client.d.ts +33 -0
- package/lib/client.js +382 -0
- package/lib/index.d.ts +7 -0
- package/lib/index.js +1246 -0
- package/lib/storage.d.ts +24 -0
- package/lib/storage.js +172 -0
- package/lib/types.d.ts +49 -0
- package/lib/types.js +2 -0
- package/package.json +45 -0
package/lib/storage.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Report, Scope, Status } from './types';
|
|
2
|
+
export declare function extendModel(ctx: any): void;
|
|
3
|
+
export declare class ReportStore {
|
|
4
|
+
private db;
|
|
5
|
+
constructor(db: any);
|
|
6
|
+
private decode;
|
|
7
|
+
private encode;
|
|
8
|
+
get(id: string): Promise<Report | undefined>;
|
|
9
|
+
all(): Promise<Report[]>;
|
|
10
|
+
findActive(scope: Scope): Promise<Report | undefined>;
|
|
11
|
+
findWaiting(scope: Scope): Promise<Report | undefined>;
|
|
12
|
+
findCurrent(scope: Scope): Promise<Report | undefined>;
|
|
13
|
+
create(task: Report): Promise<boolean>;
|
|
14
|
+
claimLog(task: Report): Promise<boolean>;
|
|
15
|
+
update(id: string, patch?: Record<string, any>, status?: undefined, releaseActive?: boolean, expected?: Status[]): Promise<Report | undefined>;
|
|
16
|
+
update(id: string, patch: Record<string, any>, status: Status, releaseActive: boolean, expected: Status[]): Promise<Report | undefined>;
|
|
17
|
+
transition(id: string, status: Status, expected: Status[], patch?: Record<string, any>, releaseActive?: boolean): Promise<Report | undefined>;
|
|
18
|
+
transitionAtRevision(id: string, status: Status, expected: Status[], revision: number, patch?: Record<string, any>, releaseActive?: boolean): Promise<Report | undefined>;
|
|
19
|
+
private write;
|
|
20
|
+
listActive(): Promise<Report[]>;
|
|
21
|
+
remove(id: string): Promise<void>;
|
|
22
|
+
prune(before: number): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export declare function activeKey(scope: Scope): string;
|
package/lib/storage.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ReportStore = void 0;
|
|
4
|
+
exports.extendModel = extendModel;
|
|
5
|
+
exports.activeKey = activeKey;
|
|
6
|
+
const TABLE = 'cnb_report_tasks';
|
|
7
|
+
const ACTIVE = new Set([
|
|
8
|
+
'WAITING_LOG', 'PREPARING_LOG', 'CREATING_ISSUE', 'TRIGGERING_NPC',
|
|
9
|
+
'WAITING_NPC', 'DELIVERING', 'AWAITING_RECOVERY', 'CLOSING_ISSUE', 'UNCERTAIN',
|
|
10
|
+
]);
|
|
11
|
+
const INDEX_FIELDS = new Set([
|
|
12
|
+
'id', 'active_key', 'status', 'platform', 'bot_id', 'guild_id', 'channel_id',
|
|
13
|
+
'user_id', 'direct', 'created_at', 'updated_at', 'deadline', 'revision',
|
|
14
|
+
]);
|
|
15
|
+
function extendModel(ctx) {
|
|
16
|
+
ctx.model.extend(TABLE, {
|
|
17
|
+
id: { type: 'string', length: 36 },
|
|
18
|
+
active_key: { type: 'string', length: 768, nullable: true },
|
|
19
|
+
status: { type: 'string', length: 32 },
|
|
20
|
+
platform: { type: 'string', length: 64 },
|
|
21
|
+
bot_id: { type: 'string', length: 128 },
|
|
22
|
+
guild_id: { type: 'string', length: 256 },
|
|
23
|
+
channel_id: { type: 'string', length: 256 },
|
|
24
|
+
user_id: { type: 'string', length: 256 },
|
|
25
|
+
direct: 'boolean',
|
|
26
|
+
created_at: 'double',
|
|
27
|
+
updated_at: 'double',
|
|
28
|
+
deadline: 'double',
|
|
29
|
+
revision: { type: 'unsigned', initial: 0 },
|
|
30
|
+
payload: 'json',
|
|
31
|
+
}, { primary: 'id', autoInc: false, unique: ['active_key'] });
|
|
32
|
+
}
|
|
33
|
+
class ReportStore {
|
|
34
|
+
db;
|
|
35
|
+
constructor(db) {
|
|
36
|
+
this.db = db;
|
|
37
|
+
}
|
|
38
|
+
decode(row) {
|
|
39
|
+
const payload = row?.payload && typeof row.payload === 'object' ? row.payload : {};
|
|
40
|
+
return { ...payload, ...row, payload: undefined };
|
|
41
|
+
}
|
|
42
|
+
encode(task) {
|
|
43
|
+
const payload = {};
|
|
44
|
+
for (const [key, value] of Object.entries(task)) {
|
|
45
|
+
if (!INDEX_FIELDS.has(key) && value !== undefined)
|
|
46
|
+
payload[key] = value;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
id: task.id,
|
|
50
|
+
active_key: task.active_key ?? null,
|
|
51
|
+
status: task.status,
|
|
52
|
+
platform: task.platform,
|
|
53
|
+
bot_id: task.bot_id,
|
|
54
|
+
guild_id: task.guild_id ?? '',
|
|
55
|
+
channel_id: task.channel_id,
|
|
56
|
+
user_id: task.user_id,
|
|
57
|
+
direct: !!task.direct,
|
|
58
|
+
created_at: task.created_at,
|
|
59
|
+
updated_at: task.updated_at,
|
|
60
|
+
deadline: task.deadline,
|
|
61
|
+
revision: Number(task.revision || 0),
|
|
62
|
+
payload,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
async get(id) {
|
|
66
|
+
const [row] = await this.db.get(TABLE, { id });
|
|
67
|
+
return row ? this.decode(row) : undefined;
|
|
68
|
+
}
|
|
69
|
+
async all() {
|
|
70
|
+
return (await this.db.get(TABLE, {})).map((row) => this.decode(row));
|
|
71
|
+
}
|
|
72
|
+
async findActive(scope) {
|
|
73
|
+
const key = activeKey(scope);
|
|
74
|
+
const [row] = await this.db.get(TABLE, { active_key: key });
|
|
75
|
+
return row ? this.decode(row) : undefined;
|
|
76
|
+
}
|
|
77
|
+
async findWaiting(scope) {
|
|
78
|
+
const task = await this.findActive(scope);
|
|
79
|
+
return task?.status === 'WAITING_LOG' ? task : undefined;
|
|
80
|
+
}
|
|
81
|
+
async findCurrent(scope) {
|
|
82
|
+
const active = await this.findActive(scope);
|
|
83
|
+
if (active)
|
|
84
|
+
return active;
|
|
85
|
+
const rows = await this.db.get(TABLE, {
|
|
86
|
+
platform: scope.platform,
|
|
87
|
+
bot_id: scope.bot_id,
|
|
88
|
+
guild_id: scope.guild_id,
|
|
89
|
+
user_id: scope.user_id,
|
|
90
|
+
}, { sort: { created_at: 'desc' }, limit: 1 });
|
|
91
|
+
return rows[0] ? this.decode(rows[0]) : undefined;
|
|
92
|
+
}
|
|
93
|
+
async create(task) {
|
|
94
|
+
try {
|
|
95
|
+
await this.db.create(TABLE, this.encode(task));
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
// The unique active_key index arbitrates simultaneous debug requests.
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async claimLog(task) {
|
|
104
|
+
return !!(await this.transition(task.id, 'PREPARING_LOG', ['WAITING_LOG']));
|
|
105
|
+
}
|
|
106
|
+
async update(id, patch = {}, status, releaseActive = false, expected) {
|
|
107
|
+
return (await this.write(id, patch, status, releaseActive, expected)).task;
|
|
108
|
+
}
|
|
109
|
+
async transition(id, status, expected, patch = {}, releaseActive = false) {
|
|
110
|
+
const result = await this.write(id, patch, status, releaseActive, expected);
|
|
111
|
+
return result.applied ? result.task : undefined;
|
|
112
|
+
}
|
|
113
|
+
async transitionAtRevision(id, status, expected, revision, patch = {}, releaseActive = false) {
|
|
114
|
+
const result = await this.write(id, patch, status, releaseActive, expected, revision);
|
|
115
|
+
return result.applied ? result.task : undefined;
|
|
116
|
+
}
|
|
117
|
+
async write(id, patch, status, releaseActive, expected, expectedRevision) {
|
|
118
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
119
|
+
const current = await this.get(id);
|
|
120
|
+
if (!current)
|
|
121
|
+
return { applied: false };
|
|
122
|
+
if (expected && !expected.includes(current.status))
|
|
123
|
+
return { task: current, applied: false };
|
|
124
|
+
if (status && status !== current.status && !expected) {
|
|
125
|
+
return { task: current, applied: false };
|
|
126
|
+
}
|
|
127
|
+
const revision = Number(current.revision || 0);
|
|
128
|
+
if (expectedRevision !== undefined && revision !== expectedRevision) {
|
|
129
|
+
return { task: current, applied: false };
|
|
130
|
+
}
|
|
131
|
+
const next = { ...current, ...patch, status: status ?? current.status };
|
|
132
|
+
if (releaseActive)
|
|
133
|
+
next.active_key = null;
|
|
134
|
+
next.updated_at = Date.now() / 1000;
|
|
135
|
+
next.revision = revision + 1;
|
|
136
|
+
const query = { id, revision };
|
|
137
|
+
if (expected)
|
|
138
|
+
query.status = { $in: expected };
|
|
139
|
+
// Minato forbids including a primary key in an update, even unchanged.
|
|
140
|
+
const { id: _id, ...update } = this.encode(next);
|
|
141
|
+
const result = await this.db.set(TABLE, query, update);
|
|
142
|
+
if (result?.matched)
|
|
143
|
+
return { task: await this.get(id), applied: true };
|
|
144
|
+
// A concurrent write changed the record after the read. Retry ordinary
|
|
145
|
+
// patches against the latest row; guarded transitions stop on a mismatch.
|
|
146
|
+
const latest = await this.get(id);
|
|
147
|
+
if (!latest || (expected && !expected.includes(latest.status))
|
|
148
|
+
|| (expectedRevision !== undefined && Number(latest.revision || 0) !== expectedRevision)) {
|
|
149
|
+
return { task: latest, applied: false };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return { task: await this.get(id), applied: false };
|
|
153
|
+
}
|
|
154
|
+
async listActive() {
|
|
155
|
+
return (await this.all()).filter(task => ACTIVE.has(task.status));
|
|
156
|
+
}
|
|
157
|
+
async remove(id) {
|
|
158
|
+
await this.db.remove(TABLE, { id });
|
|
159
|
+
}
|
|
160
|
+
async prune(before) {
|
|
161
|
+
const terminal = new Set(['DONE', 'EXPIRED', 'CANCELLED', 'FAILED']);
|
|
162
|
+
for (const task of await this.all()) {
|
|
163
|
+
if (!task.active_key && terminal.has(task.status) && task.updated_at < before) {
|
|
164
|
+
await this.remove(task.id);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
exports.ReportStore = ReportStore;
|
|
170
|
+
function activeKey(scope) {
|
|
171
|
+
return [scope.platform, scope.bot_id, scope.guild_id, scope.user_id].join(':');
|
|
172
|
+
}
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface Config {
|
|
2
|
+
group_whitelist: string[];
|
|
3
|
+
private_whitelist: string[];
|
|
4
|
+
reply_in_disabled_groups: boolean;
|
|
5
|
+
cnb_repository: string;
|
|
6
|
+
cnb_token: string;
|
|
7
|
+
cnb_api_endpoint: string;
|
|
8
|
+
cnb_web_endpoint: string;
|
|
9
|
+
npc_mention: string;
|
|
10
|
+
npc_author_ids: string[];
|
|
11
|
+
npc_author_usernames: string[];
|
|
12
|
+
assistant_name: string;
|
|
13
|
+
log_location_hint: string;
|
|
14
|
+
log_wait_minutes: number;
|
|
15
|
+
analysis_wait_minutes: number;
|
|
16
|
+
recovery_confirm_minutes: number;
|
|
17
|
+
max_log_file_mib: number;
|
|
18
|
+
poll_interval_seconds: number;
|
|
19
|
+
issue_check_interval_seconds: number;
|
|
20
|
+
delivery_send_timeout_seconds: number;
|
|
21
|
+
max_delivery_attempts: number;
|
|
22
|
+
file_url_host_allowlist: string[];
|
|
23
|
+
history_retention_days: number;
|
|
24
|
+
}
|
|
25
|
+
export interface Scope {
|
|
26
|
+
platform: string;
|
|
27
|
+
bot_id: string;
|
|
28
|
+
guild_id: string;
|
|
29
|
+
channel_id: string;
|
|
30
|
+
user_id: string;
|
|
31
|
+
direct: boolean;
|
|
32
|
+
}
|
|
33
|
+
export type Status = 'WAITING_LOG' | 'PREPARING_LOG' | 'CREATING_ISSUE' | 'TRIGGERING_NPC' | 'WAITING_NPC' | 'DELIVERING' | 'AWAITING_RECOVERY' | 'CLOSING_ISSUE' | 'UNCERTAIN' | 'DONE' | 'EXPIRED' | 'CANCELLED' | 'FAILED';
|
|
34
|
+
export interface Report {
|
|
35
|
+
id: string;
|
|
36
|
+
active_key: string | null;
|
|
37
|
+
status: Status;
|
|
38
|
+
platform: string;
|
|
39
|
+
bot_id: string;
|
|
40
|
+
guild_id: string;
|
|
41
|
+
channel_id: string;
|
|
42
|
+
user_id: string;
|
|
43
|
+
direct: boolean;
|
|
44
|
+
created_at: number;
|
|
45
|
+
updated_at: number;
|
|
46
|
+
deadline: number;
|
|
47
|
+
revision?: number;
|
|
48
|
+
[key: string]: any;
|
|
49
|
+
}
|
package/lib/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "koishi-plugin-cnb-bot",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Koishi plugin for submitting diagnostic logs to CNB Issues and relaying NPC analysis.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/HarcoChen/cnb-bot-koishi.git"
|
|
8
|
+
},
|
|
9
|
+
"main": "lib/index.js",
|
|
10
|
+
"types": "lib/index.d.ts",
|
|
11
|
+
"files": [
|
|
12
|
+
"lib",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc -p tsconfig.json",
|
|
18
|
+
"prepare": "npm run build",
|
|
19
|
+
"pack:check": "node scripts/check-package.cjs"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"koishi",
|
|
23
|
+
"cnb",
|
|
24
|
+
"qq",
|
|
25
|
+
"debug",
|
|
26
|
+
"issue"
|
|
27
|
+
],
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"koishi": "^4.18.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^22.0.0",
|
|
33
|
+
"koishi": "^4.18.0",
|
|
34
|
+
"typescript": "^5.6.0"
|
|
35
|
+
},
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18.0.0"
|
|
39
|
+
},
|
|
40
|
+
"koishi": {
|
|
41
|
+
"service": {
|
|
42
|
+
"required": ["database"]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|