botmux 2.51.1 → 2.53.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/README.en.md +22 -265
- package/README.md +21 -296
- package/dist/adapters/backend/session-backend-selector.d.ts +5 -1
- package/dist/adapters/backend/session-backend-selector.d.ts.map +1 -1
- package/dist/adapters/backend/session-backend-selector.js +15 -1
- package/dist/adapters/backend/session-backend-selector.js.map +1 -1
- package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-backend.js +3 -0
- package/dist/adapters/backend/tmux-backend.js.map +1 -1
- package/dist/adapters/backend/types.d.ts +22 -0
- package/dist/adapters/backend/types.d.ts.map +1 -1
- package/dist/adapters/backend/types.js +7 -1
- package/dist/adapters/backend/types.js.map +1 -1
- package/dist/adapters/backend/zellij-backend.d.ts +132 -0
- package/dist/adapters/backend/zellij-backend.d.ts.map +1 -0
- package/dist/adapters/backend/zellij-backend.js +375 -0
- package/dist/adapters/backend/zellij-backend.js.map +1 -0
- package/dist/adapters/backend/zellij-observe-backend.d.ts +62 -0
- package/dist/adapters/backend/zellij-observe-backend.d.ts.map +1 -0
- package/dist/adapters/backend/zellij-observe-backend.js +218 -0
- package/dist/adapters/backend/zellij-observe-backend.js.map +1 -0
- package/dist/adapters/cli/claude-code.d.ts +39 -5
- package/dist/adapters/cli/claude-code.d.ts.map +1 -1
- package/dist/adapters/cli/claude-code.js +53 -31
- package/dist/adapters/cli/claude-code.js.map +1 -1
- package/dist/adapters/cli/registry.d.ts +2 -1
- package/dist/adapters/cli/registry.d.ts.map +1 -1
- package/dist/adapters/cli/registry.js +3 -1
- package/dist/adapters/cli/registry.js.map +1 -1
- package/dist/adapters/cli/seed.d.ts +29 -0
- package/dist/adapters/cli/seed.d.ts.map +1 -0
- package/dist/adapters/cli/seed.js +63 -0
- package/dist/adapters/cli/seed.js.map +1 -0
- package/dist/adapters/cli/types.d.ts +17 -1
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/bot-registry.d.ts +31 -1
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +31 -0
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +37 -27
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +7 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/core/ask-hook/registry.d.ts.map +1 -1
- package/dist/core/ask-hook/registry.js +4 -0
- package/dist/core/ask-hook/registry.js.map +1 -1
- package/dist/core/command-handler.d.ts +4 -1
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +100 -8
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +37 -0
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/dispatch.d.ts +33 -0
- package/dist/core/dispatch.d.ts.map +1 -1
- package/dist/core/dispatch.js +26 -0
- package/dist/core/dispatch.js.map +1 -1
- package/dist/core/session-discovery.d.ts +13 -4
- package/dist/core/session-discovery.d.ts.map +1 -1
- package/dist/core/session-discovery.js +5 -5
- package/dist/core/session-discovery.js.map +1 -1
- package/dist/core/session-manager.d.ts +10 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +43 -18
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/types.d.ts +5 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/worker-pool.d.ts +1 -1
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +22 -9
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/core/zellij-adopt-discovery.d.ts +28 -0
- package/dist/core/zellij-adopt-discovery.d.ts.map +1 -0
- package/dist/core/zellij-adopt-discovery.js +255 -0
- package/dist/core/zellij-adopt-discovery.js.map +1 -0
- package/dist/core/zellij-session-discovery.d.ts +73 -0
- package/dist/core/zellij-session-discovery.d.ts.map +1 -0
- package/dist/core/zellij-session-discovery.js +259 -0
- package/dist/core/zellij-session-discovery.js.map +1 -0
- package/dist/daemon.d.ts +3 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +145 -13
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults.js +114 -0
- package/dist/dashboard/web/bot-defaults.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +23 -1
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard/web/sessions.d.ts.map +1 -1
- package/dist/dashboard/web/sessions.js +1 -0
- package/dist/dashboard/web/sessions.js.map +1 -1
- package/dist/dashboard/web/workflows.js +1 -1
- package/dist/dashboard/web/workflows.js.map +1 -1
- package/dist/dashboard-web/app.js +449 -426
- package/dist/dashboard.js +20 -0
- package/dist/dashboard.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +15 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +16 -2
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/card-builder.d.ts +8 -3
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +74 -5
- package/dist/im/lark/card-builder.js.map +1 -1
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +72 -10
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts +12 -0
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +39 -31
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/im/lark/grant-command.d.ts +26 -0
- package/dist/im/lark/grant-command.d.ts.map +1 -1
- package/dist/im/lark/grant-command.js +142 -3
- package/dist/im/lark/grant-command.js.map +1 -1
- package/dist/im/lark/grant-pending.d.ts +7 -4
- package/dist/im/lark/grant-pending.d.ts.map +1 -1
- package/dist/im/lark/grant-pending.js +12 -6
- package/dist/im/lark/grant-pending.js.map +1 -1
- package/dist/services/codex-app-threads.d.ts +20 -0
- package/dist/services/codex-app-threads.d.ts.map +1 -0
- package/dist/services/codex-app-threads.js +165 -0
- package/dist/services/codex-app-threads.js.map +1 -0
- package/dist/services/grant-prefs-store.d.ts +23 -0
- package/dist/services/grant-prefs-store.d.ts.map +1 -0
- package/dist/services/grant-prefs-store.js +94 -0
- package/dist/services/grant-prefs-store.js.map +1 -0
- package/dist/services/grant-store.d.ts +34 -2
- package/dist/services/grant-store.d.ts.map +1 -1
- package/dist/services/grant-store.js +160 -9
- package/dist/services/grant-store.js.map +1 -1
- package/dist/services/quota-dedup.d.ts +33 -0
- package/dist/services/quota-dedup.d.ts.map +1 -0
- package/dist/services/quota-dedup.js +67 -0
- package/dist/services/quota-dedup.js.map +1 -0
- package/dist/setup/bot-config-editor.d.ts +1 -1
- package/dist/setup/bot-config-editor.d.ts.map +1 -1
- package/dist/setup/bot-config-editor.js +5 -4
- package/dist/setup/bot-config-editor.js.map +1 -1
- package/dist/setup/ensure-zellij.d.ts +48 -0
- package/dist/setup/ensure-zellij.d.ts.map +1 -0
- package/dist/setup/ensure-zellij.js +93 -0
- package/dist/setup/ensure-zellij.js.map +1 -0
- package/dist/types.d.ts +9 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/anchor-serializer.d.ts +3 -2
- package/dist/utils/anchor-serializer.d.ts.map +1 -1
- package/dist/utils/anchor-serializer.js +20 -5
- package/dist/utils/anchor-serializer.js.map +1 -1
- package/dist/utils/transient-snapshot.js +2 -2
- package/dist/utils/transient-snapshot.js.map +1 -1
- package/dist/worker.js +124 -30
- package/dist/worker.js.map +1 -1
- package/dist/workflows/attempt-resume.d.ts +1 -1
- package/dist/workflows/attempt-resume.d.ts.map +1 -1
- package/dist/workflows/attempt-resume.js +1 -1
- package/dist/workflows/attempt-resume.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codex-app-threads.d.ts","sourceRoot":"","sources":["../../src/services/codex-app-threads.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAuID,wBAAsB,mBAAmB,CAAC,IAAI,GAAE,0BAA+B,GAAG,OAAO,CAAC,qBAAqB,EAAE,CAAC,CAqBjH"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { resolveCommand } from '../adapters/cli/registry.js';
|
|
3
|
+
class CodexAppServerProbe {
|
|
4
|
+
child;
|
|
5
|
+
nextId = 1;
|
|
6
|
+
stdoutBuffer = '';
|
|
7
|
+
pending = new Map();
|
|
8
|
+
lastStderr = '';
|
|
9
|
+
closed = false;
|
|
10
|
+
constructor(codexBin, cwd) {
|
|
11
|
+
this.child = spawn(codexBin, ['app-server', '--listen', 'stdio://'], {
|
|
12
|
+
cwd,
|
|
13
|
+
env: process.env,
|
|
14
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
15
|
+
});
|
|
16
|
+
this.child.stdout.on('data', chunk => this.onStdout(chunk.toString('utf8')));
|
|
17
|
+
this.child.stderr.on('data', chunk => {
|
|
18
|
+
this.lastStderr = (this.lastStderr + chunk.toString('utf8')).slice(-8000);
|
|
19
|
+
});
|
|
20
|
+
this.child.on('error', err => this.failAll(new Error(`Failed to start Codex app-server: ${err.message}`)));
|
|
21
|
+
this.child.on('exit', (code, signal) => {
|
|
22
|
+
this.closed = true;
|
|
23
|
+
this.failAll(new Error(`Codex app-server exited (code=${code}, signal=${signal})${this.lastStderr ? `\n${this.lastStderr}` : ''}`));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
async initialize(timeoutMs) {
|
|
27
|
+
await this.withTimeout(this.request('initialize', {
|
|
28
|
+
clientInfo: { name: 'botmux-codex-app-thread-picker', version: '0.0.0' },
|
|
29
|
+
capabilities: {
|
|
30
|
+
experimentalApi: true,
|
|
31
|
+
suppressNotifications: ['thread/started', 'thread/status/changed'],
|
|
32
|
+
},
|
|
33
|
+
}), timeoutMs, 'initialize');
|
|
34
|
+
this.notify('initialized');
|
|
35
|
+
}
|
|
36
|
+
request(method, params) {
|
|
37
|
+
if (this.closed)
|
|
38
|
+
return Promise.reject(new Error('Codex app-server is closed'));
|
|
39
|
+
const id = this.nextId++;
|
|
40
|
+
this.child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
this.pending.set(id, { method, resolve, reject });
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
notify(method, params) {
|
|
46
|
+
if (this.closed)
|
|
47
|
+
return;
|
|
48
|
+
const msg = { jsonrpc: '2.0', method };
|
|
49
|
+
if (params !== undefined)
|
|
50
|
+
msg.params = params;
|
|
51
|
+
this.child.stdin.write(JSON.stringify(msg) + '\n');
|
|
52
|
+
}
|
|
53
|
+
close() {
|
|
54
|
+
this.closed = true;
|
|
55
|
+
try {
|
|
56
|
+
this.child.kill();
|
|
57
|
+
}
|
|
58
|
+
catch { /* already gone */ }
|
|
59
|
+
this.failAll(new Error('Codex app-server probe closed'));
|
|
60
|
+
}
|
|
61
|
+
async withTimeout(promise, timeoutMs, label) {
|
|
62
|
+
let timer;
|
|
63
|
+
try {
|
|
64
|
+
return await Promise.race([
|
|
65
|
+
promise,
|
|
66
|
+
new Promise((_, reject) => {
|
|
67
|
+
timer = setTimeout(() => reject(new Error(`Codex app-server ${label} timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
68
|
+
}),
|
|
69
|
+
]);
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
if (timer)
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
onStdout(data) {
|
|
77
|
+
this.stdoutBuffer += data;
|
|
78
|
+
for (;;) {
|
|
79
|
+
const nl = this.stdoutBuffer.indexOf('\n');
|
|
80
|
+
if (nl < 0)
|
|
81
|
+
return;
|
|
82
|
+
const line = this.stdoutBuffer.slice(0, nl).trim();
|
|
83
|
+
this.stdoutBuffer = this.stdoutBuffer.slice(nl + 1);
|
|
84
|
+
if (!line)
|
|
85
|
+
continue;
|
|
86
|
+
let msg;
|
|
87
|
+
try {
|
|
88
|
+
msg = JSON.parse(line);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (typeof msg.id === 'number' && (msg.result !== undefined || msg.error !== undefined)) {
|
|
94
|
+
const pending = this.pending.get(msg.id);
|
|
95
|
+
if (!pending)
|
|
96
|
+
continue;
|
|
97
|
+
this.pending.delete(msg.id);
|
|
98
|
+
if (msg.error)
|
|
99
|
+
pending.reject(new Error(`${pending.method}: ${JSON.stringify(msg.error)}`));
|
|
100
|
+
else
|
|
101
|
+
pending.resolve(msg.result);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
failAll(err) {
|
|
106
|
+
for (const pending of this.pending.values())
|
|
107
|
+
pending.reject(err);
|
|
108
|
+
this.pending.clear();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function stringifyStatus(status) {
|
|
112
|
+
if (!status)
|
|
113
|
+
return undefined;
|
|
114
|
+
if (typeof status === 'string')
|
|
115
|
+
return status;
|
|
116
|
+
if (typeof status === 'object' && typeof status.type === 'string')
|
|
117
|
+
return status.type;
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
function normalizeThread(raw) {
|
|
121
|
+
const threadId = typeof raw.id === 'string' ? raw.id : undefined;
|
|
122
|
+
const cwd = typeof raw.cwd === 'string' ? raw.cwd : undefined;
|
|
123
|
+
if (!threadId || !cwd)
|
|
124
|
+
return null;
|
|
125
|
+
const updatedAt = typeof raw.updatedAt === 'number' ? raw.updatedAt : undefined;
|
|
126
|
+
const createdAt = typeof raw.createdAt === 'number' ? raw.createdAt : undefined;
|
|
127
|
+
const name = typeof raw.name === 'string' && raw.name.trim() ? raw.name.trim() : undefined;
|
|
128
|
+
const preview = typeof raw.preview === 'string' ? raw.preview.trim() : '';
|
|
129
|
+
const source = typeof raw.source === 'string' ? raw.source : undefined;
|
|
130
|
+
return {
|
|
131
|
+
threadId,
|
|
132
|
+
name,
|
|
133
|
+
preview,
|
|
134
|
+
cwd,
|
|
135
|
+
updatedAtMs: updatedAt !== undefined ? updatedAt * 1000 : undefined,
|
|
136
|
+
createdAtMs: createdAt !== undefined ? createdAt * 1000 : undefined,
|
|
137
|
+
path: typeof raw.path === 'string' ? raw.path : undefined,
|
|
138
|
+
source,
|
|
139
|
+
status: stringifyStatus(raw.status),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
export async function listCodexAppThreads(opts = {}) {
|
|
143
|
+
const timeoutMs = opts.timeoutMs ?? 7000;
|
|
144
|
+
const codexBin = resolveCommand(opts.codexBin ?? 'codex');
|
|
145
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
146
|
+
const client = new CodexAppServerProbe(codexBin, cwd);
|
|
147
|
+
try {
|
|
148
|
+
await client.initialize(timeoutMs);
|
|
149
|
+
const result = await client.withTimeout(client.request('thread/list', {
|
|
150
|
+
limit: opts.limit ?? 30,
|
|
151
|
+
sortKey: 'updated_at',
|
|
152
|
+
sortDirection: 'desc',
|
|
153
|
+
archived: false,
|
|
154
|
+
useStateDbOnly: true,
|
|
155
|
+
searchTerm: opts.searchTerm && opts.searchTerm.trim() ? opts.searchTerm.trim() : null,
|
|
156
|
+
}), timeoutMs, 'thread/list');
|
|
157
|
+
const rows = Array.isArray(result?.data) ? result.data : [];
|
|
158
|
+
const normalized = rows.map((row) => normalizeThread(row));
|
|
159
|
+
return normalized.filter((thread) => !!thread);
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
client.close();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=codex-app-threads.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codex-app-threads.js","sourceRoot":"","sources":["../../src/services/codex-app-threads.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface BotGrantPrefs {
|
|
2
|
+
/** owner 限制被授权人只能纯对话、拦截一切 slash 命令。默认 false。 */
|
|
3
|
+
restrictGrantCommands: boolean;
|
|
4
|
+
/** 消息额度默认值:null = 关闭(无限);正整数 = 不带数字的 /grant 取此值。 */
|
|
5
|
+
messageQuotaDefaultLimit: number | null;
|
|
6
|
+
}
|
|
7
|
+
/** Current grant prefs for a bot(缺省 restrict=false、quota=null)。 */
|
|
8
|
+
export declare function getBotGrantPrefs(larkAppId: string): BotGrantPrefs;
|
|
9
|
+
/**
|
|
10
|
+
* 持久化一次 grant-prefs 局部修改。只动 patch 里出现的 key。
|
|
11
|
+
* • restrictGrantCommands=false → 删 key(bots.json 保持干净,缺省即默认)
|
|
12
|
+
* • messageQuotaDefaultLimit=null → 删整个 messageQuota(关闭默认额度;不动 quotaState 计数)
|
|
13
|
+
* • messageQuotaDefaultLimit=正整数 → 写入;非法值(非整数/0/负数)直接拒,返回 bad_quota
|
|
14
|
+
* 返回写后解析出的完整 prefs。
|
|
15
|
+
*/
|
|
16
|
+
export declare function updateBotGrantPrefs(larkAppId: string, patch: Partial<BotGrantPrefs>): Promise<{
|
|
17
|
+
ok: true;
|
|
18
|
+
prefs: BotGrantPrefs;
|
|
19
|
+
} | {
|
|
20
|
+
ok: false;
|
|
21
|
+
reason: string;
|
|
22
|
+
}>;
|
|
23
|
+
//# sourceMappingURL=grant-prefs-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grant-prefs-store.d.ts","sourceRoot":"","sources":["../../src/services/grant-prefs-store.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,aAAa;IAC5B,gDAAgD;IAChD,qBAAqB,EAAE,OAAO,CAAC;IAC/B,oDAAoD;IACpD,wBAAwB,EAAE,MAAM,GAAG,IAAI,CAAC;CACzC;AAQD,mEAAmE;AACnE,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,CAUjE;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,OAAO,CAAC,aAAa,CAAC,GAC5B,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,aAAa,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAiD7E"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-bot 授权(/grant)相关偏好。与 card-prefs-store / brand-store 同款:
|
|
3
|
+
* 跨进程文件锁 + bots.json 原子写,外加内存 registry 同步,让 daemon 的
|
|
4
|
+
* 路由 / grant 处理不必重启即可生效。
|
|
5
|
+
*
|
|
6
|
+
* 两个独立设置:
|
|
7
|
+
* • restrictGrantCommands — owner 开关:被授权人只能纯对话,拦截一切 slash 命令
|
|
8
|
+
* • messageQuota.defaultLimit — 消息额度默认值。字段「是否存在」本身就是额度机制
|
|
9
|
+
* 总开关:缺省 = 关闭(无限);正整数 = 不带数字的
|
|
10
|
+
* `/grant @x` 取此值。显式 `/grant @x N` 恒生效,与此无关。
|
|
11
|
+
*/
|
|
12
|
+
import { rmwBotEntry } from './config-store.js';
|
|
13
|
+
import { getBot } from '../bot-registry.js';
|
|
14
|
+
import { logger } from '../utils/logger.js';
|
|
15
|
+
/** 把 entry.messageQuota.defaultLimit 归一成 number|null(只认正整数,其余视作关闭)。 */
|
|
16
|
+
function readQuotaLimit(c) {
|
|
17
|
+
const d = c.messageQuota?.defaultLimit;
|
|
18
|
+
return typeof d === 'number' && Number.isInteger(d) && d > 0 ? d : null;
|
|
19
|
+
}
|
|
20
|
+
/** Current grant prefs for a bot(缺省 restrict=false、quota=null)。 */
|
|
21
|
+
export function getBotGrantPrefs(larkAppId) {
|
|
22
|
+
try {
|
|
23
|
+
const c = getBot(larkAppId).config;
|
|
24
|
+
return {
|
|
25
|
+
restrictGrantCommands: c.restrictGrantCommands === true,
|
|
26
|
+
messageQuotaDefaultLimit: readQuotaLimit(c),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return { restrictGrantCommands: false, messageQuotaDefaultLimit: null };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 持久化一次 grant-prefs 局部修改。只动 patch 里出现的 key。
|
|
35
|
+
* • restrictGrantCommands=false → 删 key(bots.json 保持干净,缺省即默认)
|
|
36
|
+
* • messageQuotaDefaultLimit=null → 删整个 messageQuota(关闭默认额度;不动 quotaState 计数)
|
|
37
|
+
* • messageQuotaDefaultLimit=正整数 → 写入;非法值(非整数/0/负数)直接拒,返回 bad_quota
|
|
38
|
+
* 返回写后解析出的完整 prefs。
|
|
39
|
+
*/
|
|
40
|
+
export async function updateBotGrantPrefs(larkAppId, patch) {
|
|
41
|
+
let bot;
|
|
42
|
+
try {
|
|
43
|
+
bot = getBot(larkAppId);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return { ok: false, reason: 'bot_not_registered' };
|
|
47
|
+
}
|
|
48
|
+
// 额度值校验:null 表示关闭;否则必须是正整数。
|
|
49
|
+
if (patch.messageQuotaDefaultLimit !== undefined && patch.messageQuotaDefaultLimit !== null) {
|
|
50
|
+
const n = patch.messageQuotaDefaultLimit;
|
|
51
|
+
if (typeof n !== 'number' || !Number.isInteger(n) || n <= 0) {
|
|
52
|
+
return { ok: false, reason: 'bad_quota' };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const r = await rmwBotEntry(larkAppId, (entry) => {
|
|
56
|
+
if (patch.restrictGrantCommands !== undefined) {
|
|
57
|
+
if (patch.restrictGrantCommands)
|
|
58
|
+
entry.restrictGrantCommands = true;
|
|
59
|
+
else
|
|
60
|
+
delete entry.restrictGrantCommands;
|
|
61
|
+
}
|
|
62
|
+
if (patch.messageQuotaDefaultLimit !== undefined) {
|
|
63
|
+
if (patch.messageQuotaDefaultLimit === null) {
|
|
64
|
+
// 关闭默认额度只删 messageQuota.defaultLimit 这个开关,保留 quotaState 计数。
|
|
65
|
+
delete entry.messageQuota;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
entry.messageQuota = { ...(entry.messageQuota ?? {}), defaultLimit: patch.messageQuotaDefaultLimit };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
write: true,
|
|
73
|
+
result: {
|
|
74
|
+
restrictGrantCommands: entry.restrictGrantCommands === true,
|
|
75
|
+
messageQuotaDefaultLimit: readQuotaLimit(entry),
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
if (!r.ok)
|
|
80
|
+
return { ok: false, reason: r.reason };
|
|
81
|
+
// 同步内存 config,路由 / grant 处理不重启即生效。
|
|
82
|
+
if (patch.restrictGrantCommands !== undefined) {
|
|
83
|
+
bot.config.restrictGrantCommands = patch.restrictGrantCommands || undefined;
|
|
84
|
+
}
|
|
85
|
+
if (patch.messageQuotaDefaultLimit !== undefined) {
|
|
86
|
+
bot.config.messageQuota = patch.messageQuotaDefaultLimit === null
|
|
87
|
+
? undefined
|
|
88
|
+
: { defaultLimit: patch.messageQuotaDefaultLimit };
|
|
89
|
+
}
|
|
90
|
+
logger.info(`[grant-prefs:${larkAppId}] restrictGrantCommands=${r.result.restrictGrantCommands} ` +
|
|
91
|
+
`messageQuotaDefaultLimit=${r.result.messageQuotaDefaultLimit ?? 'off'}`);
|
|
92
|
+
return { ok: true, prefs: r.result };
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=grant-prefs-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grant-prefs-store.js","sourceRoot":"","sources":["../../src/services/grant-prefs-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,7 +2,9 @@ type Fail = {
|
|
|
2
2
|
ok: false;
|
|
3
3
|
reason: string;
|
|
4
4
|
};
|
|
5
|
-
export declare function
|
|
5
|
+
export declare function chatQuotaKey(chatId: string, openId: string): string;
|
|
6
|
+
export declare function globalQuotaKey(openId: string): string;
|
|
7
|
+
export declare function addChatGrant(larkAppId: string, chatId: string, openId: string, quota?: number): Promise<{
|
|
6
8
|
ok: true;
|
|
7
9
|
created: boolean;
|
|
8
10
|
} | Fail>;
|
|
@@ -10,10 +12,40 @@ export declare function addChatGrant(larkAppId: string, chatId: string, openId:
|
|
|
10
12
|
* 全局对话授权:把 open_id 加入 globalGrants(被授权在任意群与本 bot 对话的名单,人/bot 通用)。
|
|
11
13
|
* talk-only —— 只进 canTalk / bot 路由闸,绝不写 allowedUsers、不授 canOperate(与 addChatGrant 同源)。
|
|
12
14
|
*/
|
|
13
|
-
export declare function addGlobalGrant(larkAppId: string, openId: string): Promise<{
|
|
15
|
+
export declare function addGlobalGrant(larkAppId: string, openId: string, quota?: number): Promise<{
|
|
14
16
|
ok: true;
|
|
15
17
|
created: boolean;
|
|
16
18
|
} | Fail>;
|
|
19
|
+
/**
|
|
20
|
+
* scope-aware talk-only 移除:删本群 chatGrants[chatId] 中的 openId + 其 chat quota 记录。
|
|
21
|
+
* 额度用尽/崩溃自愈时调。不碰 allowedUsers、不碰 globalGrants,故无 would_open_bot 守卫。
|
|
22
|
+
*/
|
|
23
|
+
export declare function removeChatGrant(larkAppId: string, chatId: string, openId: string): Promise<{
|
|
24
|
+
ok: true;
|
|
25
|
+
removed: boolean;
|
|
26
|
+
} | Fail>;
|
|
27
|
+
/**
|
|
28
|
+
* scope-aware talk-only 移除:删 globalGrants 中的 openId + 其 global quota 记录。
|
|
29
|
+
* talk-only,不碰 allowedUsers,无 would_open_bot 守卫(清空它不放大 operate)。
|
|
30
|
+
*/
|
|
31
|
+
export declare function removeGlobalGrant(larkAppId: string, openId: string): Promise<{
|
|
32
|
+
ok: true;
|
|
33
|
+
removed: boolean;
|
|
34
|
+
} | Fail>;
|
|
35
|
+
/**
|
|
36
|
+
* 扣一次额度(一条对话输入)。RMW 锁内递增、内存以锁内磁盘快照 used 为准。
|
|
37
|
+
* 无记录 → tracked:false(无需 enforce,放行)。
|
|
38
|
+
* 已达/超上限 → allow:false(应拦本条 + 自愈 revoke)。
|
|
39
|
+
* 正常递增 → allow:true,exhausted=(used 恰好达 limit),调用方放行本条、若 exhausted 则处理后 revoke。
|
|
40
|
+
* 基础设施失败(getBot / RMW)会 throw —— 调用方 catch 后 fail-closed(拒发以保硬上限)。
|
|
41
|
+
*/
|
|
42
|
+
export declare function consumeQuota(larkAppId: string, quotaKey: string): Promise<{
|
|
43
|
+
tracked: boolean;
|
|
44
|
+
allow: boolean;
|
|
45
|
+
exhausted: boolean;
|
|
46
|
+
used: number;
|
|
47
|
+
limit: number;
|
|
48
|
+
}>;
|
|
17
49
|
/**
|
|
18
50
|
* 整群 talk 授权:把 chatId 加入 allowedChatGroups("talk-open 的 chat_id 列表")。
|
|
19
51
|
* 命中后该 chat 任何成员都过 canTalk(见 event-dispatcher.canTalk),不授 canOperate。
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grant-store.d.ts","sourceRoot":"","sources":["../../src/services/grant-store.ts"],"names":[],"mappings":"AAQA,KAAK,IAAI,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"grant-store.d.ts","sourceRoot":"","sources":["../../src/services/grant-store.ts"],"names":[],"mappings":"AAQA,KAAK,IAAI,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAK1C,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAAuC;AAC3G,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAA+B;AA0CrF,wBAAsB,YAAY,CAChC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAChE,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAAC,CAsBhD;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAChD,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAAC,CAkBhD;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAChD,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAAC,CAuBhD;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAChC,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAAC,CAsBhD;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAClC,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAiBhG;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAChC,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAAC,CAehD;AAED,oDAAoD;AACpD,wBAAsB,sBAAsB,CAC1C,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAChC,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAAC,CAkBhD;AAED;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAC/B,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAChD,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,OAAO,CAAC;QAAC,UAAU,EAAE,OAAO,CAAA;KAAE,CAAA;CAAE,GAAG,IAAI,CAAC,CAqEhG"}
|
|
@@ -5,6 +5,39 @@
|
|
|
5
5
|
import { getBot, getOwnerOpenId } from '../bot-registry.js';
|
|
6
6
|
import { rmwBotEntry } from './config-store.js';
|
|
7
7
|
import { logger } from '../utils/logger.js';
|
|
8
|
+
// ─── 消息额度(scope-aware quota)─────────────────────────────────────────────
|
|
9
|
+
// quotaKey 的单一格式来源:chat 授权按群+人,global 授权按人。evaluateTalk 拼 key 时
|
|
10
|
+
// 复用这两个 builder,保证 store / enforce 两侧格式一致。
|
|
11
|
+
export function chatQuotaKey(chatId, openId) { return `chat:${chatId}:${openId}`; }
|
|
12
|
+
export function globalQuotaKey(openId) { return `global:${openId}`; }
|
|
13
|
+
/** 取 quotaState(容错:非对象/数组 → undefined)。entry(磁盘原始)与 bot.config 同形,通用。 */
|
|
14
|
+
function getQuotaMap(o) {
|
|
15
|
+
return (o.quotaState && typeof o.quotaState === 'object' && !Array.isArray(o.quotaState)) ? o.quotaState : undefined;
|
|
16
|
+
}
|
|
17
|
+
/** 写/删一条 quota 记录(rec=null 删)。返回是否实际改动。空 map 删键。 */
|
|
18
|
+
function setQuotaRecord(o, qk, rec) {
|
|
19
|
+
const qs = getQuotaMap(o);
|
|
20
|
+
if (rec) {
|
|
21
|
+
o.quotaState = { ...(qs ?? {}), [qk]: rec };
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
if (qs && qk in qs) {
|
|
25
|
+
const next = { ...qs };
|
|
26
|
+
delete next[qk];
|
|
27
|
+
if (Object.keys(next).length > 0)
|
|
28
|
+
o.quotaState = next;
|
|
29
|
+
else
|
|
30
|
+
delete o.quotaState;
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
/** 授权时套额度:quota>0 → 重置为 {limit,used:0}(续杯语义);undefined → 删记录(转无限)。 */
|
|
36
|
+
function applyGrantQuota(o, qk, quota) {
|
|
37
|
+
return quota !== undefined && quota > 0
|
|
38
|
+
? setQuotaRecord(o, qk, { limit: quota, used: 0 })
|
|
39
|
+
: setQuotaRecord(o, qk, null);
|
|
40
|
+
}
|
|
8
41
|
/** 把目标 open_id 映射回 allowedUsers 里的 raw 条目(可能是 email,也可能就是 open_id)。 */
|
|
9
42
|
function rawEntryForOpenId(larkAppId, openId) {
|
|
10
43
|
const bot = getBot(larkAppId);
|
|
@@ -19,7 +52,7 @@ function rawEntryForOpenId(larkAppId, openId) {
|
|
|
19
52
|
function resolvedAfterRemoval(larkAppId, openId) {
|
|
20
53
|
return getBot(larkAppId).resolvedAllowedUsers.filter(u => u !== openId);
|
|
21
54
|
}
|
|
22
|
-
export async function addChatGrant(larkAppId, chatId, openId) {
|
|
55
|
+
export async function addChatGrant(larkAppId, chatId, openId, quota) {
|
|
23
56
|
let bot;
|
|
24
57
|
try {
|
|
25
58
|
bot = getBot(larkAppId);
|
|
@@ -27,6 +60,7 @@ export async function addChatGrant(larkAppId, chatId, openId) {
|
|
|
27
60
|
catch {
|
|
28
61
|
return { ok: false, reason: 'bot_not_registered' };
|
|
29
62
|
}
|
|
63
|
+
const qk = chatQuotaKey(chatId, openId);
|
|
30
64
|
const r = await rmwBotEntry(larkAppId, (entry) => {
|
|
31
65
|
const map = (entry.chatGrants && typeof entry.chatGrants === 'object') ? entry.chatGrants : {};
|
|
32
66
|
const cur = Array.isArray(map[chatId]) ? map[chatId] : [];
|
|
@@ -35,22 +69,26 @@ export async function addChatGrant(larkAppId, chatId, openId) {
|
|
|
35
69
|
cur.push(openId);
|
|
36
70
|
map[chatId] = cur;
|
|
37
71
|
entry.chatGrants = map;
|
|
38
|
-
|
|
72
|
+
// 带额度即(重)设记录;无额度则删除已有记录(转无限)。重新授权 = 续杯/重置。
|
|
73
|
+
const qChanged = applyGrantQuota(entry, qk, quota);
|
|
74
|
+
return { write: created || qChanged, result: { created } };
|
|
39
75
|
});
|
|
40
76
|
if (!r.ok)
|
|
41
77
|
return r;
|
|
42
78
|
if (r.result.created) {
|
|
43
79
|
const map = (bot.config.chatGrants ??= {});
|
|
44
|
-
|
|
45
|
-
|
|
80
|
+
if (!map[chatId]?.includes(openId))
|
|
81
|
+
map[chatId] = [...(map[chatId] ?? []), openId];
|
|
46
82
|
}
|
|
83
|
+
applyGrantQuota(bot.config, qk, quota); // 同步内存
|
|
84
|
+
logger.info(`[grant:${larkAppId}] +chat ${chatId} ${openId}${quota ? ` quota=${quota}` : ''}`);
|
|
47
85
|
return { ok: true, created: r.result.created };
|
|
48
86
|
}
|
|
49
87
|
/**
|
|
50
88
|
* 全局对话授权:把 open_id 加入 globalGrants(被授权在任意群与本 bot 对话的名单,人/bot 通用)。
|
|
51
89
|
* talk-only —— 只进 canTalk / bot 路由闸,绝不写 allowedUsers、不授 canOperate(与 addChatGrant 同源)。
|
|
52
90
|
*/
|
|
53
|
-
export async function addGlobalGrant(larkAppId, openId) {
|
|
91
|
+
export async function addGlobalGrant(larkAppId, openId, quota) {
|
|
54
92
|
let bot;
|
|
55
93
|
try {
|
|
56
94
|
bot = getBot(larkAppId);
|
|
@@ -58,22 +96,129 @@ export async function addGlobalGrant(larkAppId, openId) {
|
|
|
58
96
|
catch {
|
|
59
97
|
return { ok: false, reason: 'bot_not_registered' };
|
|
60
98
|
}
|
|
99
|
+
const qk = globalQuotaKey(openId);
|
|
61
100
|
const r = await rmwBotEntry(larkAppId, (entry) => {
|
|
62
101
|
const cur = Array.isArray(entry.globalGrants) ? entry.globalGrants : [];
|
|
63
102
|
const created = !cur.includes(openId);
|
|
64
103
|
if (created)
|
|
65
104
|
cur.push(openId);
|
|
66
105
|
entry.globalGrants = cur;
|
|
67
|
-
|
|
106
|
+
const qChanged = applyGrantQuota(entry, qk, quota);
|
|
107
|
+
return { write: created || qChanged, result: { created } };
|
|
68
108
|
});
|
|
69
109
|
if (!r.ok)
|
|
70
110
|
return r;
|
|
71
|
-
if (r.result.created) {
|
|
111
|
+
if (r.result.created && !bot.config.globalGrants?.includes(openId)) {
|
|
72
112
|
bot.config.globalGrants = [...(bot.config.globalGrants ?? []), openId];
|
|
73
|
-
logger.info(`[grant:${larkAppId}] +global ${openId}`);
|
|
74
113
|
}
|
|
114
|
+
applyGrantQuota(bot.config, qk, quota); // 同步内存
|
|
115
|
+
logger.info(`[grant:${larkAppId}] +global ${openId}${quota ? ` quota=${quota}` : ''}`);
|
|
75
116
|
return { ok: true, created: r.result.created };
|
|
76
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* scope-aware talk-only 移除:删本群 chatGrants[chatId] 中的 openId + 其 chat quota 记录。
|
|
120
|
+
* 额度用尽/崩溃自愈时调。不碰 allowedUsers、不碰 globalGrants,故无 would_open_bot 守卫。
|
|
121
|
+
*/
|
|
122
|
+
export async function removeChatGrant(larkAppId, chatId, openId) {
|
|
123
|
+
let bot;
|
|
124
|
+
try {
|
|
125
|
+
bot = getBot(larkAppId);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return { ok: false, reason: 'bot_not_registered' };
|
|
129
|
+
}
|
|
130
|
+
const qk = chatQuotaKey(chatId, openId);
|
|
131
|
+
const r = await rmwBotEntry(larkAppId, (entry) => {
|
|
132
|
+
let removed = false;
|
|
133
|
+
const map = (entry.chatGrants && typeof entry.chatGrants === 'object') ? entry.chatGrants : {};
|
|
134
|
+
if (Array.isArray(map[chatId]) && map[chatId].includes(openId)) {
|
|
135
|
+
map[chatId] = map[chatId].filter((u) => u !== openId);
|
|
136
|
+
if (map[chatId].length === 0)
|
|
137
|
+
delete map[chatId];
|
|
138
|
+
entry.chatGrants = map;
|
|
139
|
+
removed = true;
|
|
140
|
+
}
|
|
141
|
+
const qChanged = setQuotaRecord(entry, qk, null);
|
|
142
|
+
return { write: removed || qChanged, result: { removed } };
|
|
143
|
+
});
|
|
144
|
+
if (!r.ok)
|
|
145
|
+
return r;
|
|
146
|
+
if (r.result.removed && bot.config.chatGrants?.[chatId]) {
|
|
147
|
+
bot.config.chatGrants[chatId] = bot.config.chatGrants[chatId].filter(u => u !== openId);
|
|
148
|
+
if (bot.config.chatGrants[chatId].length === 0)
|
|
149
|
+
delete bot.config.chatGrants[chatId];
|
|
150
|
+
}
|
|
151
|
+
setQuotaRecord(bot.config, qk, null);
|
|
152
|
+
logger.info(`[grant:${larkAppId}] -chat ${chatId} ${openId} (quota exhausted/heal)`);
|
|
153
|
+
return { ok: true, removed: r.result.removed };
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* scope-aware talk-only 移除:删 globalGrants 中的 openId + 其 global quota 记录。
|
|
157
|
+
* talk-only,不碰 allowedUsers,无 would_open_bot 守卫(清空它不放大 operate)。
|
|
158
|
+
*/
|
|
159
|
+
export async function removeGlobalGrant(larkAppId, openId) {
|
|
160
|
+
let bot;
|
|
161
|
+
try {
|
|
162
|
+
bot = getBot(larkAppId);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return { ok: false, reason: 'bot_not_registered' };
|
|
166
|
+
}
|
|
167
|
+
const qk = globalQuotaKey(openId);
|
|
168
|
+
const r = await rmwBotEntry(larkAppId, (entry) => {
|
|
169
|
+
let removed = false;
|
|
170
|
+
const gg = Array.isArray(entry.globalGrants) ? entry.globalGrants : [];
|
|
171
|
+
if (gg.includes(openId)) {
|
|
172
|
+
const next = gg.filter((u) => u !== openId);
|
|
173
|
+
if (next.length > 0)
|
|
174
|
+
entry.globalGrants = next;
|
|
175
|
+
else
|
|
176
|
+
delete entry.globalGrants;
|
|
177
|
+
removed = true;
|
|
178
|
+
}
|
|
179
|
+
const qChanged = setQuotaRecord(entry, qk, null);
|
|
180
|
+
return { write: removed || qChanged, result: { removed } };
|
|
181
|
+
});
|
|
182
|
+
if (!r.ok)
|
|
183
|
+
return r;
|
|
184
|
+
if (r.result.removed) {
|
|
185
|
+
const next = (bot.config.globalGrants ?? []).filter(u => u !== openId);
|
|
186
|
+
if (next.length > 0)
|
|
187
|
+
bot.config.globalGrants = next;
|
|
188
|
+
else
|
|
189
|
+
delete bot.config.globalGrants;
|
|
190
|
+
}
|
|
191
|
+
setQuotaRecord(bot.config, qk, null);
|
|
192
|
+
logger.info(`[grant:${larkAppId}] -global ${openId} (quota exhausted/heal)`);
|
|
193
|
+
return { ok: true, removed: r.result.removed };
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* 扣一次额度(一条对话输入)。RMW 锁内递增、内存以锁内磁盘快照 used 为准。
|
|
197
|
+
* 无记录 → tracked:false(无需 enforce,放行)。
|
|
198
|
+
* 已达/超上限 → allow:false(应拦本条 + 自愈 revoke)。
|
|
199
|
+
* 正常递增 → allow:true,exhausted=(used 恰好达 limit),调用方放行本条、若 exhausted 则处理后 revoke。
|
|
200
|
+
* 基础设施失败(getBot / RMW)会 throw —— 调用方 catch 后 fail-closed(拒发以保硬上限)。
|
|
201
|
+
*/
|
|
202
|
+
export async function consumeQuota(larkAppId, quotaKey) {
|
|
203
|
+
const bot = getBot(larkAppId); // throw → 调用方 fail-closed
|
|
204
|
+
const r = await rmwBotEntry(larkAppId, (entry) => {
|
|
205
|
+
const qs = getQuotaMap(entry);
|
|
206
|
+
const rec = qs?.[quotaKey];
|
|
207
|
+
if (!rec)
|
|
208
|
+
return { write: false, result: { tracked: false, allow: true, exhausted: false, used: 0, limit: 0 } };
|
|
209
|
+
if (rec.used >= rec.limit) {
|
|
210
|
+
return { write: false, result: { tracked: true, allow: false, exhausted: true, used: rec.used, limit: rec.limit } };
|
|
211
|
+
}
|
|
212
|
+
const used = rec.used + 1;
|
|
213
|
+
entry.quotaState = { ...qs, [quotaKey]: { limit: rec.limit, used } };
|
|
214
|
+
return { write: true, result: { tracked: true, allow: true, exhausted: used >= rec.limit, used, limit: rec.limit } };
|
|
215
|
+
});
|
|
216
|
+
if (!r.ok)
|
|
217
|
+
throw new Error(`consumeQuota RMW failed: ${r.reason}`);
|
|
218
|
+
if (r.result.tracked)
|
|
219
|
+
setQuotaRecord(bot.config, quotaKey, { limit: r.result.limit, used: r.result.used });
|
|
220
|
+
return r.result;
|
|
221
|
+
}
|
|
77
222
|
/**
|
|
78
223
|
* 整群 talk 授权:把 chatId 加入 allowedChatGroups("talk-open 的 chat_id 列表")。
|
|
79
224
|
* 命中后该 chat 任何成员都过 canTalk(见 event-dispatcher.canTalk),不授 canOperate。
|
|
@@ -188,7 +333,10 @@ export async function revokeGrant(larkAppId, chatId, openId) {
|
|
|
188
333
|
delete entry.globalGrants;
|
|
189
334
|
globalTalk = true;
|
|
190
335
|
}
|
|
191
|
-
|
|
336
|
+
// 手动 /revoke 一并清两 scope 的额度记录(与三支授权同 RMW 原子)。
|
|
337
|
+
const qChat = setQuotaRecord(entry, chatQuotaKey(chatId, openId), null);
|
|
338
|
+
const qGlobal = setQuotaRecord(entry, globalQuotaKey(openId), null);
|
|
339
|
+
return { write: chat || global || globalTalk || qChat || qGlobal, result: { chat, global, globalTalk } };
|
|
192
340
|
});
|
|
193
341
|
if (!r.ok)
|
|
194
342
|
return r;
|
|
@@ -214,6 +362,9 @@ export async function revokeGrant(larkAppId, chatId, openId) {
|
|
|
214
362
|
else
|
|
215
363
|
delete bot.config.globalGrants;
|
|
216
364
|
}
|
|
365
|
+
// 同步内存额度记录(两 scope)
|
|
366
|
+
setQuotaRecord(bot.config, chatQuotaKey(chatId, openId), null);
|
|
367
|
+
setQuotaRecord(bot.config, globalQuotaKey(openId), null);
|
|
217
368
|
logger.info(`[grant:${larkAppId}] revoke chat=${chatId} ${openId} removed=${JSON.stringify(r.result)}`);
|
|
218
369
|
return { ok: true, removed: r.result };
|
|
219
370
|
}
|