@quecue/dev-notify 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.en.md +89 -0
- package/CHANGELOG.md +15 -0
- package/README.en.md +275 -0
- package/README.md +9 -2
- package/SECURITY.en.md +78 -0
- package/SECURITY.md +4 -2
- package/dist/channels.js +14 -12
- package/dist/claude-code.js +30 -30
- package/dist/claude-hook.js +23 -23
- package/dist/cli.js +106 -81
- package/dist/config.js +37 -20
- package/dist/credentials.js +14 -13
- package/dist/formatting.js +6 -4
- package/dist/i18n.js +960 -0
- package/dist/maintenance.js +17 -16
- package/dist/prompts.js +7 -6
- package/dist/reply-bridge.js +11 -10
- package/dist/reply-engine.js +20 -19
- package/dist/reply-hooks.js +21 -20
- package/dist/reply-interactions.js +36 -35
- package/dist/reply-outcomes.js +46 -23
- package/dist/reply-platform.js +14 -13
- package/dist/reply-state.js +31 -30
- package/dist/reply-telegram.js +4 -3
- package/dist/setup.js +69 -51
- package/dist/update.js +23 -22
- package/package.json +4 -1
package/dist/maintenance.js
CHANGED
|
@@ -3,17 +3,18 @@ import { fileURLToPath } from 'node:url';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
5
|
import { ConfigError } from './config.js';
|
|
6
|
+
import { t } from './i18n.js';
|
|
6
7
|
import { claudeCodeStatus } from './claude-code.js';
|
|
7
8
|
import { bridgeStatus } from './reply-bridge.js';
|
|
8
9
|
function runCli(args) {
|
|
9
10
|
const cli = fileURLToPath(new URL('./cli.js', import.meta.url));
|
|
10
11
|
const result = spawnSync(process.execPath, [cli, ...args], { stdio: 'inherit', timeout: 300000 });
|
|
11
12
|
if (result.error)
|
|
12
|
-
throw new ConfigError('
|
|
13
|
+
throw new ConfigError(t('maintenance.runFailed'));
|
|
13
14
|
return result.status ?? 1;
|
|
14
15
|
}
|
|
15
16
|
function reason(error) {
|
|
16
|
-
return error instanceof ConfigError ? error.message : '
|
|
17
|
+
return error instanceof ConfigError ? error.message : t('maintenance.reasonHint');
|
|
17
18
|
}
|
|
18
19
|
const normalize = (file) => process.platform === 'win32' ? path.resolve(file).toLowerCase() : path.resolve(file);
|
|
19
20
|
export async function postUpdate(options) {
|
|
@@ -27,28 +28,28 @@ export async function postUpdate(options) {
|
|
|
27
28
|
hooksStatus = await hooks();
|
|
28
29
|
}
|
|
29
30
|
catch (error) {
|
|
30
|
-
options.err(
|
|
31
|
+
options.err(t('maintenance.hooksCheckFailed', { reason: reason(error) }));
|
|
31
32
|
failed = true;
|
|
32
33
|
}
|
|
33
34
|
if (hooksStatus) {
|
|
34
35
|
const count = hooksStatus.stop + hooksStatus.notification + hooksStatus.lifecycle + hooksStatus.interaction;
|
|
35
36
|
if (!count)
|
|
36
|
-
options.out('
|
|
37
|
+
options.out(t('maintenance.noHooks'));
|
|
37
38
|
else if (!hooksStatus.configs.some(file => normalize(file) === normalize(options.config))) {
|
|
38
|
-
options.out('
|
|
39
|
+
options.out(t('maintenance.otherConfig'));
|
|
39
40
|
}
|
|
40
41
|
else {
|
|
41
|
-
options.out('
|
|
42
|
+
options.out(t('maintenance.updatingHooks'));
|
|
42
43
|
try {
|
|
43
44
|
if (run(['integration', 'claude-code', 'install', '--yes', '--config', options.config]) === 0)
|
|
44
|
-
options.out('
|
|
45
|
+
options.out(t('maintenance.hooksUpdated'));
|
|
45
46
|
else {
|
|
46
|
-
options.err('
|
|
47
|
+
options.err(t('maintenance.hooksFailed'));
|
|
47
48
|
failed = true;
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
catch (error) {
|
|
51
|
-
options.err(
|
|
52
|
+
options.err(t('maintenance.hooksFailedReason', { reason: reason(error) }));
|
|
52
53
|
failed = true;
|
|
53
54
|
}
|
|
54
55
|
}
|
|
@@ -58,11 +59,11 @@ export async function postUpdate(options) {
|
|
|
58
59
|
bridgeState = await bridge();
|
|
59
60
|
}
|
|
60
61
|
catch (error) {
|
|
61
|
-
options.err(
|
|
62
|
+
options.err(t('maintenance.bridgeCheckFailed', { reason: reason(error) }));
|
|
62
63
|
failed = true;
|
|
63
64
|
}
|
|
64
65
|
if (bridgeState?.running) {
|
|
65
|
-
options.out('
|
|
66
|
+
options.out(t('maintenance.restartingBridge'));
|
|
66
67
|
try {
|
|
67
68
|
// 0 表示已停止或本来未运行;1 表示停止请求已登记,下面等待进程释放锁。
|
|
68
69
|
run(['reply', 'stop', '--config', options.config]);
|
|
@@ -79,22 +80,22 @@ export async function postUpdate(options) {
|
|
|
79
80
|
await wait(500);
|
|
80
81
|
}
|
|
81
82
|
if (stillRunning) {
|
|
82
|
-
options.err('
|
|
83
|
+
options.err(t('maintenance.bridgeStillRunning'));
|
|
83
84
|
failed = true;
|
|
84
85
|
}
|
|
85
86
|
else if (run(['reply', 'start', '--config', options.config]) === 0)
|
|
86
|
-
options.out('
|
|
87
|
+
options.out(t('maintenance.bridgeRestarted'));
|
|
87
88
|
else {
|
|
88
|
-
options.err('
|
|
89
|
+
options.err(t('maintenance.bridgeStartFailed'));
|
|
89
90
|
failed = true;
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
93
|
catch (error) {
|
|
93
|
-
options.err(
|
|
94
|
+
options.err(t('maintenance.bridgeRestartFailed', { reason: reason(error) }));
|
|
94
95
|
failed = true;
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
98
|
else if (bridgeState)
|
|
98
|
-
options.out('
|
|
99
|
+
options.out(t('maintenance.noBridge'));
|
|
99
100
|
return failed ? 1 : 0;
|
|
100
101
|
}
|
package/dist/prompts.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { emitKeypressEvents } from 'node:readline';
|
|
2
2
|
import { createInterface } from 'node:readline/promises';
|
|
3
3
|
import { ConfigError } from './config.js';
|
|
4
|
+
import { t } from './i18n.js';
|
|
4
5
|
export class Cancelled extends Error {
|
|
5
6
|
}
|
|
6
7
|
export function terminalPrompts() {
|
|
7
8
|
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
8
|
-
throw new ConfigError('
|
|
9
|
+
throw new ConfigError(t('prompts.terminalRequired'));
|
|
9
10
|
return {
|
|
10
11
|
out: text => console.log(text),
|
|
11
12
|
text: label => readText(label),
|
|
@@ -22,7 +23,7 @@ export async function readText(label, input = process.stdin, output = process.st
|
|
|
22
23
|
return (await terminal.question(label, { signal: controller.signal })).trim();
|
|
23
24
|
}
|
|
24
25
|
catch {
|
|
25
|
-
throw new Cancelled('
|
|
26
|
+
throw new Cancelled(t('prompts.cancelled'));
|
|
26
27
|
}
|
|
27
28
|
finally {
|
|
28
29
|
terminal.off('SIGINT', cancel);
|
|
@@ -32,7 +33,7 @@ export async function readText(label, input = process.stdin, output = process.st
|
|
|
32
33
|
}
|
|
33
34
|
export async function readHidden(label, input = process.stdin, output = process.stdout) {
|
|
34
35
|
if (!input.isTTY || !output.isTTY || typeof input.setRawMode !== 'function')
|
|
35
|
-
throw new ConfigError('
|
|
36
|
+
throw new ConfigError(t('prompts.hiddenUnsupported'));
|
|
36
37
|
const wasRaw = input.isRaw, wasPaused = input.isPaused();
|
|
37
38
|
return new Promise((resolve, reject) => {
|
|
38
39
|
let value = '';
|
|
@@ -58,8 +59,8 @@ export async function readHidden(label, input = process.stdin, output = process.
|
|
|
58
59
|
value = '';
|
|
59
60
|
error ? reject(error) : resolve(result);
|
|
60
61
|
};
|
|
61
|
-
const failed = () => finish(new ConfigError('
|
|
62
|
-
const cancelled = () => finish(new Cancelled('
|
|
62
|
+
const failed = () => finish(new ConfigError(t('prompts.hiddenIncomplete')));
|
|
63
|
+
const cancelled = () => finish(new Cancelled(t('prompts.cancelled')));
|
|
63
64
|
const keypress = (text, key = {}) => {
|
|
64
65
|
if (key.ctrl && (key.name === 'c' || key.name === 'd'))
|
|
65
66
|
return cancelled();
|
|
@@ -76,7 +77,7 @@ export async function readHidden(label, input = process.stdin, output = process.
|
|
|
76
77
|
if (key.ctrl || key.meta || !text || /[\x00-\x1f\x7f]/.test(text))
|
|
77
78
|
return;
|
|
78
79
|
if (value.length + text.length > 8192)
|
|
79
|
-
return finish(new ConfigError('
|
|
80
|
+
return finish(new ConfigError(t('prompts.inputTooLong')));
|
|
80
81
|
value += text;
|
|
81
82
|
};
|
|
82
83
|
try {
|
package/dist/reply-bridge.js
CHANGED
|
@@ -5,6 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import { randomBytes } from 'node:crypto';
|
|
6
6
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
7
7
|
import { appPaths, ConfigError } from './config.js';
|
|
8
|
+
import { t } from './i18n.js';
|
|
8
9
|
import { accessReplies, atomicJson, readJson, replyDirectory, stateTransaction } from './reply-state.js';
|
|
9
10
|
import { telegramApi } from './reply-telegram.js';
|
|
10
11
|
import { drainReplies, processUpdate } from './reply-engine.js';
|
|
@@ -44,7 +45,7 @@ export async function stopBridge(file) {
|
|
|
44
45
|
export async function startBridge(file, env = process.env) {
|
|
45
46
|
const access = await accessReplies(file, env);
|
|
46
47
|
if (!access)
|
|
47
|
-
throw new ConfigError('
|
|
48
|
+
throw new ConfigError(t('reply.bridgeAuthFirst'));
|
|
48
49
|
const existing = await bridgeStatus(file);
|
|
49
50
|
if (existing.running)
|
|
50
51
|
return existing.ready;
|
|
@@ -72,7 +73,7 @@ export async function runBridge(file, options = {}) {
|
|
|
72
73
|
const env = options.env ?? process.env;
|
|
73
74
|
let access = await accessReplies(file, env);
|
|
74
75
|
if (!access)
|
|
75
|
-
throw new ConfigError('
|
|
76
|
+
throw new ConfigError(t('reply.bridgeNotAuthorized'));
|
|
76
77
|
const directory = replyDirectory(file);
|
|
77
78
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
78
79
|
const botDirectory = path.join(appPaths(process.platform, env).data, 'reply-bots');
|
|
@@ -83,7 +84,7 @@ export async function runBridge(file, options = {}) {
|
|
|
83
84
|
lock = await open(lockPath, 'wx', 0o600);
|
|
84
85
|
}
|
|
85
86
|
catch {
|
|
86
|
-
throw new ConfigError('
|
|
87
|
+
throw new ConfigError(t('reply.bridgeLocked'));
|
|
87
88
|
}
|
|
88
89
|
const owner = randomBytes(16).toString('hex'), lease = { pid: process.pid, owner, ready: false, updated: Date.now() };
|
|
89
90
|
let stopping = false;
|
|
@@ -100,7 +101,7 @@ export async function runBridge(file, options = {}) {
|
|
|
100
101
|
let api = (options.api ?? (value => telegramApi(value.config.telegram.token)))(access);
|
|
101
102
|
const webhook = await api('getWebhookInfo', {});
|
|
102
103
|
if (webhook?.url)
|
|
103
|
-
throw new ConfigError('
|
|
104
|
+
throw new ConfigError(t('reply.webhookConflict'));
|
|
104
105
|
await api('getMe', {});
|
|
105
106
|
const currentState = await stateTransaction(access, state => ({ offset: state.offset, empty: Object.keys(state.entries).length === 0 }));
|
|
106
107
|
if (!currentState.offset && currentState.empty) {
|
|
@@ -137,11 +138,11 @@ export async function runBridge(file, options = {}) {
|
|
|
137
138
|
updates = await api('getUpdates', { offset, timeout: 2, allowed_updates: ['message', 'callback_query'] });
|
|
138
139
|
}
|
|
139
140
|
catch (error) {
|
|
140
|
-
if (error instanceof ConfigError &&
|
|
141
|
+
if (error instanceof ConfigError && /冲突|conflict/i.test(error.message))
|
|
141
142
|
throw error;
|
|
142
|
-
log('
|
|
143
|
+
log(t('reply.pollFailedLog'));
|
|
143
144
|
if (++failures >= 5)
|
|
144
|
-
throw new ConfigError('
|
|
145
|
+
throw new ConfigError(t('reply.pollFailedFatal'));
|
|
145
146
|
lease.updated = Date.now();
|
|
146
147
|
await atomicJson(leasePath(file), lease);
|
|
147
148
|
await (options.sleep ?? delay)(3000);
|
|
@@ -152,7 +153,7 @@ export async function runBridge(file, options = {}) {
|
|
|
152
153
|
if (!after || after.identity !== access.identity || await isStopped())
|
|
153
154
|
break;
|
|
154
155
|
if (!Array.isArray(updates))
|
|
155
|
-
throw new ConfigError('
|
|
156
|
+
throw new ConfigError(t('reply.updatesInvalid'));
|
|
156
157
|
for (const update of updates) {
|
|
157
158
|
if (await isStopped())
|
|
158
159
|
break;
|
|
@@ -160,7 +161,7 @@ export async function runBridge(file, options = {}) {
|
|
|
160
161
|
await processUpdate(access, update, context);
|
|
161
162
|
}
|
|
162
163
|
catch {
|
|
163
|
-
log('
|
|
164
|
+
log(t('reply.handleFailedLog'));
|
|
164
165
|
}
|
|
165
166
|
}
|
|
166
167
|
if (updates.length)
|
|
@@ -173,7 +174,7 @@ export async function runBridge(file, options = {}) {
|
|
|
173
174
|
}
|
|
174
175
|
catch (error) {
|
|
175
176
|
lease.ready = false;
|
|
176
|
-
lease.error = error instanceof ConfigError ? error.message : '
|
|
177
|
+
lease.error = error instanceof ConfigError ? error.message : t('reply.bridgeFailed');
|
|
177
178
|
await atomicJson(leasePath(file), lease).catch(() => { });
|
|
178
179
|
throw error;
|
|
179
180
|
}
|
package/dist/reply-engine.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import { ConfigError } from './config.js';
|
|
3
|
+
import { t } from './i18n.js';
|
|
3
4
|
import { digest, targetEpoch, stateTransaction, TTL } from './reply-state.js';
|
|
4
5
|
import { handleInteractionCallback, handleInteractionText, showInteraction } from './reply-interactions.js';
|
|
5
6
|
import { injectReply, sanitizeReply, validateTarget } from './reply-platform.js';
|
|
@@ -27,7 +28,7 @@ export async function resolveEntry(access, key, context) {
|
|
|
27
28
|
async function prompt(access, chat, messageId, key, context, autoReply = true) {
|
|
28
29
|
const resolved = await resolveEntry(access, key, context);
|
|
29
30
|
if (!resolved) {
|
|
30
|
-
await replyHint(context.api, chat, '
|
|
31
|
+
await replyHint(context.api, chat, t('reply.sessionGoneTitle'), t('reply.sessionGoneBody'), messageId);
|
|
31
32
|
return;
|
|
32
33
|
}
|
|
33
34
|
if (resolved.entry.request) {
|
|
@@ -36,8 +37,8 @@ async function prompt(access, chat, messageId, key, context, autoReply = true) {
|
|
|
36
37
|
}
|
|
37
38
|
const session = resolved.target.title || resolved.target.session;
|
|
38
39
|
const sent = autoReply
|
|
39
|
-
? await replyHint(context.api, chat, '
|
|
40
|
-
: await replyHint(context.api, chat, '
|
|
40
|
+
? await replyHint(context.api, chat, t('reply.replyPromptTitle'), t('reply.replyPromptBody', { session }), messageId, { force_reply: true, selective: true })
|
|
41
|
+
: await replyHint(context.api, chat, t('reply.replyPromptTitle'), t('reply.replyPromptLongPress', { session }), messageId, { remove_keyboard: true });
|
|
41
42
|
if (Number.isSafeInteger(sent?.message_id))
|
|
42
43
|
await stateTransaction(access, state => { state.entries[`tg:${chat}:${sent.message_id}`] = resolved.entry; });
|
|
43
44
|
}
|
|
@@ -53,9 +54,9 @@ export async function processUpdate(access, update, context) {
|
|
|
53
54
|
if (chat && current?.identity === access.identity && current.policy.users.includes(chat)) {
|
|
54
55
|
if (typeof update?.callback_query?.id === 'string')
|
|
55
56
|
await context.api('answerCallbackQuery', {
|
|
56
|
-
callback_query_id: update.callback_query.id, text: '
|
|
57
|
+
callback_query_id: update.callback_query.id, text: t('reply.processingFailedCallback'),
|
|
57
58
|
}).catch(() => { });
|
|
58
|
-
await replyHint(context.api, chat, '
|
|
59
|
+
await replyHint(context.api, chat, t('reply.processingFailedTitle'), t('reply.processingFailedBody')).catch(() => { });
|
|
59
60
|
}
|
|
60
61
|
throw error;
|
|
61
62
|
}
|
|
@@ -83,8 +84,8 @@ async function processAuthorizedUpdate(access, update, context) {
|
|
|
83
84
|
if (typeof value === 'string' && value.startsWith('dn:q:')) {
|
|
84
85
|
const id = value.split(':')[2];
|
|
85
86
|
if (!/^[a-f0-9]{32}$/.test(id) || !await resolveEntry(access, 'token:' + id, context)) {
|
|
86
|
-
await context.api('answerCallbackQuery', { callback_query_id: callback.id, text: '
|
|
87
|
-
await replyHint(context.api, chat, '
|
|
87
|
+
await context.api('answerCallbackQuery', { callback_query_id: callback.id, text: t('reply.interactionExpiredCallback') }).catch(() => { });
|
|
88
|
+
await replyHint(context.api, chat, t('reply.interactionExpiredTitle'), t('reply.interactionExpiredBody')).catch(() => { });
|
|
88
89
|
return;
|
|
89
90
|
}
|
|
90
91
|
if (await handleInteractionCallback(access, callback, chat, context.api))
|
|
@@ -92,7 +93,7 @@ async function processAuthorizedUpdate(access, update, context) {
|
|
|
92
93
|
}
|
|
93
94
|
if (typeof value !== 'string' || !/^dn:r:[a-f0-9]{32}$/.test(value) || typeof callback.id !== 'string')
|
|
94
95
|
return;
|
|
95
|
-
await context.api('answerCallbackQuery', { callback_query_id: callback.id, text: '
|
|
96
|
+
await context.api('answerCallbackQuery', { callback_query_id: callback.id, text: t('reply.replyBoxHint') });
|
|
96
97
|
await prompt(access, chat, callback.message.message_id, 'token:' + value.slice(5), context);
|
|
97
98
|
return;
|
|
98
99
|
}
|
|
@@ -107,7 +108,7 @@ async function processAuthorizedUpdate(access, update, context) {
|
|
|
107
108
|
if (!current.policy.users.includes(chat))
|
|
108
109
|
return;
|
|
109
110
|
if (typeof message.date === 'number' && Date.now() / 1000 - message.date > 300) {
|
|
110
|
-
await replyHint(context.api, chat, '
|
|
111
|
+
await replyHint(context.api, chat, t('reply.staleTitle'), t('reply.staleBody'));
|
|
111
112
|
return;
|
|
112
113
|
}
|
|
113
114
|
if (/^\/start\s+[a-f0-9]{32}$/.test(text)) {
|
|
@@ -117,12 +118,12 @@ async function processAuthorizedUpdate(access, update, context) {
|
|
|
117
118
|
}
|
|
118
119
|
const replyTo = message.reply_to_message?.message_id;
|
|
119
120
|
if (!Number.isSafeInteger(replyTo)) {
|
|
120
|
-
await replyHint(context.api, chat, '
|
|
121
|
+
await replyHint(context.api, chat, t('reply.howToTitle'), t('reply.howToBody'), message.message_id);
|
|
121
122
|
return;
|
|
122
123
|
}
|
|
123
124
|
const resolved = await resolveEntry(access, `tg:${chat}:${replyTo}`, context);
|
|
124
125
|
if (!resolved) {
|
|
125
|
-
await replyHint(context.api, chat, '
|
|
126
|
+
await replyHint(context.api, chat, t('reply.sessionGoneTitle'), t('reply.expiredReplyBody'), message.message_id);
|
|
126
127
|
return;
|
|
127
128
|
}
|
|
128
129
|
if (resolved.entry.request) {
|
|
@@ -134,14 +135,14 @@ async function processAuthorizedUpdate(access, update, context) {
|
|
|
134
135
|
safe = sanitizeReply(message.text);
|
|
135
136
|
}
|
|
136
137
|
catch (error) {
|
|
137
|
-
await replyHint(context.api, chat, '
|
|
138
|
+
await replyHint(context.api, chat, t('reply.replyFailedTitle'), error instanceof ConfigError ? error.message : t('reply.invalidInput'), message.message_id);
|
|
138
139
|
return;
|
|
139
140
|
}
|
|
140
141
|
const id = randomBytes(16).toString('hex');
|
|
141
142
|
const queued = await stateTransaction(access, state => {
|
|
142
143
|
const target = state.sessions[resolved.target.session];
|
|
143
144
|
if (!target || sessionEpoch(target) !== resolved.entry.epoch)
|
|
144
|
-
throw new ConfigError('
|
|
145
|
+
throw new ConfigError(t('reply.sessionChanged'));
|
|
145
146
|
if (state.pending[target.session] && Date.now() - state.pending[target.session].updated < 5 * 60 * 1000)
|
|
146
147
|
return null;
|
|
147
148
|
state.pending[target.session] = { hash: textHash(safe), text: safe, epoch: resolved.entry.epoch, id, updated: Date.now(), queued: target.working,
|
|
@@ -149,11 +150,11 @@ async function processAuthorizedUpdate(access, update, context) {
|
|
|
149
150
|
return target.working;
|
|
150
151
|
});
|
|
151
152
|
if (queued === null) {
|
|
152
|
-
await replyHint(context.api, chat, '
|
|
153
|
+
await replyHint(context.api, chat, t('reply.pendingTitle'), t('reply.pendingBody'), message.message_id);
|
|
153
154
|
return;
|
|
154
155
|
}
|
|
155
156
|
if (queued)
|
|
156
|
-
await replyHint(context.api, chat, '
|
|
157
|
+
await replyHint(context.api, chat, t('reply.queuedTitle'), t('reply.queuedBody'), message.message_id);
|
|
157
158
|
else
|
|
158
159
|
await drainReplies(access, context);
|
|
159
160
|
}
|
|
@@ -170,9 +171,9 @@ export async function drainReplies(access, context) {
|
|
|
170
171
|
if (!current || current.identity !== access.identity || !current.policy.users.includes(pending.chat))
|
|
171
172
|
return;
|
|
172
173
|
if (Date.now() - pending.updated > 5 * 60 * 1000)
|
|
173
|
-
throw new ConfigError('
|
|
174
|
+
throw new ConfigError(t('reply.queueExpired'));
|
|
174
175
|
if (!target.tracked || pending.epoch !== sessionEpoch(target) || !await (context.validate ?? (value => validateTarget(value)))(target))
|
|
175
|
-
throw new ConfigError('
|
|
176
|
+
throw new ConfigError(t('reply.sessionClosed'));
|
|
176
177
|
const text = pending.text;
|
|
177
178
|
const claimed = await stateTransaction(access, state => {
|
|
178
179
|
const actual = state.pending[target.session], active = state.sessions[target.session];
|
|
@@ -189,7 +190,7 @@ export async function drainReplies(access, context) {
|
|
|
189
190
|
if (!final || final.identity !== access.identity)
|
|
190
191
|
return;
|
|
191
192
|
await (context.inject ?? ((value, content) => injectReply(value, content)))(target, text);
|
|
192
|
-
await replyHint(context.api, pending.chat, '
|
|
193
|
+
await replyHint(context.api, pending.chat, t('reply.forwardedTitle'), t('reply.forwardedBody'), pending.message);
|
|
193
194
|
}
|
|
194
195
|
catch (error) {
|
|
195
196
|
await stateTransaction(access, state => {
|
|
@@ -199,7 +200,7 @@ export async function drainReplies(access, context) {
|
|
|
199
200
|
delete value.text;
|
|
200
201
|
}
|
|
201
202
|
});
|
|
202
|
-
await replyHint(context.api, pending.chat, '
|
|
203
|
+
await replyHint(context.api, pending.chat, t('reply.forwardUnknownTitle'), error instanceof ConfigError ? error.message : t('reply.forwardFailed'), pending.message).catch(() => { });
|
|
203
204
|
}
|
|
204
205
|
}
|
|
205
206
|
}
|
package/dist/reply-hooks.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import { ConfigError } from './config.js';
|
|
3
|
+
import { t } from './i18n.js';
|
|
3
4
|
import { accessReplies, botKey, digest, stateTransaction, targetEpoch } from './reply-state.js';
|
|
4
5
|
import { findSession } from './reply-platform.js';
|
|
5
6
|
import { textHash } from './reply-engine.js';
|
|
@@ -27,13 +28,13 @@ export async function lifecycleHook(data, file, options = {}) {
|
|
|
27
28
|
if (event === 'SessionEnd' || !target) {
|
|
28
29
|
delete state.sessions[data.session_id];
|
|
29
30
|
delete state.pending[data.session_id];
|
|
30
|
-
endSessionInteractions(state, data.session_id, '
|
|
31
|
+
endSessionInteractions(state, data.session_id, t('reply.endSessionReason'));
|
|
31
32
|
return null;
|
|
32
33
|
}
|
|
33
34
|
// 同进程 / 同 TTY 已切换会话时,旧映射不再允许输入。
|
|
34
35
|
for (const [id, other] of Object.entries(state.sessions)) {
|
|
35
36
|
if (id !== target.session && (other.pid === target.pid || (target.tty && other.tty === target.tty))) {
|
|
36
|
-
endSessionInteractions(state, id, '
|
|
37
|
+
endSessionInteractions(state, id, t('reply.endSwitchedReason'));
|
|
37
38
|
delete state.sessions[id];
|
|
38
39
|
delete state.pending[id];
|
|
39
40
|
}
|
|
@@ -52,22 +53,22 @@ export async function lifecycleHook(data, file, options = {}) {
|
|
|
52
53
|
if (event === 'UserPromptSubmit' && !match && pending?.phase === 'queued')
|
|
53
54
|
delete state.pending[target.session];
|
|
54
55
|
if (event === 'UserPromptSubmit' || event === 'SessionStart' || event === 'Stop') {
|
|
55
|
-
endSessionInteractions(state, target.session, event === 'Stop' ? '
|
|
56
|
+
endSessionInteractions(state, target.session, event === 'Stop' ? t('reply.endTurnReason') : t('reply.endLocalReason'));
|
|
56
57
|
}
|
|
57
58
|
return match ? pending : null;
|
|
58
59
|
});
|
|
59
60
|
if (receipt) {
|
|
60
|
-
await replyHint(apiFor(access, options), receipt.chat, '
|
|
61
|
+
await replyHint(apiFor(access, options), receipt.chat, t('reply.receivedTitle'), t('reply.receivedBody'), receipt.message).catch(() => options.err?.(t('reply.receivedTelegramFailed')));
|
|
61
62
|
if (access.config.feishu.enabled && access.config.feishu.webhook) {
|
|
62
63
|
try {
|
|
63
|
-
const result = await (options.send ?? deliver)(makePlan('feishu', { title: '
|
|
64
|
-
content: '
|
|
65
|
-
footer: '
|
|
64
|
+
const result = await (options.send ?? deliver)(makePlan('feishu', { title: t('reply.receivedTitle'), level: 'info',
|
|
65
|
+
content: t('reply.receivedFeishuBody'),
|
|
66
|
+
footer: t('reply.receivedFeishuFooter', { session: data.session_id }) }, access.config), access.config);
|
|
66
67
|
if (!result.ok)
|
|
67
|
-
options.err?.('
|
|
68
|
+
options.err?.(t('reply.receivedFeishuIncomplete'));
|
|
68
69
|
}
|
|
69
70
|
catch {
|
|
70
|
-
options.err?.('
|
|
71
|
+
options.err?.(t('reply.receivedFeishuFailed'));
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
}
|
|
@@ -111,12 +112,12 @@ export async function attachReply(message, data, file, options = {}) {
|
|
|
111
112
|
const ready = options.bridge ? await options.bridge(file, options.env ?? process.env)
|
|
112
113
|
: (await bridgeStatus(file)).ready || await startBridge(file, options.env);
|
|
113
114
|
if (!ready) {
|
|
114
|
-
options.err?.('
|
|
115
|
+
options.err?.(t('reply.bridgeNotReadyButton'));
|
|
115
116
|
return null;
|
|
116
117
|
}
|
|
117
118
|
const api = apiFor(access, options), bot = await api('getMe', {});
|
|
118
119
|
if (typeof bot?.username !== 'string' || !/^[A-Za-z0-9_]{5,64}$/.test(bot.username))
|
|
119
|
-
throw new ConfigError('
|
|
120
|
+
throw new ConfigError(t('reply.botUsernameInvalid'));
|
|
120
121
|
const token = randomBytes(16).toString('hex');
|
|
121
122
|
await stateTransaction(access, state => { state.entries['token:' + token] = { session: target.session, epoch: targetEpoch(target), updated: Date.now() }; });
|
|
122
123
|
message.reply = { token, url: `https://t.me/${bot.username}?start=${token}`, users: access.policy.users };
|
|
@@ -129,7 +130,7 @@ export function mapReplies(plan, binding) {
|
|
|
129
130
|
if (!binding.access.policy.users.includes(item.recipient))
|
|
130
131
|
return;
|
|
131
132
|
if (!Number.isSafeInteger(response.result?.message_id))
|
|
132
|
-
throw new ConfigError('
|
|
133
|
+
throw new ConfigError(t('reply.noMessageId'));
|
|
133
134
|
await stateTransaction(binding.access, state => {
|
|
134
135
|
const entry = state.entries['token:' + binding.token];
|
|
135
136
|
if (entry)
|
|
@@ -157,7 +158,7 @@ export async function interactionHook(data, file, options = {}) {
|
|
|
157
158
|
// 登记阶段复用生命周期元数据;真正开启远程交互时仍重新核验进程身份。
|
|
158
159
|
const target = cached ?? await getTarget(data, options);
|
|
159
160
|
if (!target) {
|
|
160
|
-
options.err?.('
|
|
161
|
+
options.err?.(t('reply.noSession'));
|
|
161
162
|
return;
|
|
162
163
|
}
|
|
163
164
|
if (data.hook_event_name === 'PreToolUse') {
|
|
@@ -169,13 +170,13 @@ export async function interactionHook(data, file, options = {}) {
|
|
|
169
170
|
return;
|
|
170
171
|
const call = await stateTransaction(access, state => matchingCall(state, target, data));
|
|
171
172
|
if (!call) {
|
|
172
|
-
options.err?.('
|
|
173
|
+
options.err?.(t('reply.noMatchingCall'));
|
|
173
174
|
return;
|
|
174
175
|
}
|
|
175
176
|
const ready = options.bridge ? await options.bridge(file, options.env ?? process.env)
|
|
176
177
|
: (await bridgeStatus(file)).ready || await startBridge(file, options.env);
|
|
177
178
|
if (!ready) {
|
|
178
|
-
options.err?.('
|
|
179
|
+
options.err?.(t('reply.bridgeNotReadyQuestion'));
|
|
179
180
|
return;
|
|
180
181
|
}
|
|
181
182
|
const api = apiFor(access, options);
|
|
@@ -190,8 +191,8 @@ export async function interactionHook(data, file, options = {}) {
|
|
|
190
191
|
if (access.config.feishu.enabled && access.config.feishu.webhook) {
|
|
191
192
|
const bot = await api('getMe', {});
|
|
192
193
|
if (typeof bot?.username === 'string' && /^[A-Za-z0-9_]{5,64}$/.test(bot.username)) {
|
|
193
|
-
const result = await (options.send ?? deliver)(makePlan('feishu', { title: '
|
|
194
|
-
content: kind === 'question' ? questionsContent(request.questions) : '
|
|
194
|
+
const result = await (options.send ?? deliver)(makePlan('feishu', { title: t('hook.notificationTitle'), level: 'warn', footer: '',
|
|
195
|
+
content: kind === 'question' ? questionsContent(request.questions) : t('reply.permissionRequest', { tool: String(data.tool_name) }),
|
|
195
196
|
reply: { token: request.id, url: `https://t.me/${bot.username}?start=${request.id}`, users: access.policy.users } }, access.config), access.config);
|
|
196
197
|
if (result.ok)
|
|
197
198
|
await stateTransaction(access, state => {
|
|
@@ -207,13 +208,13 @@ export async function interactionHook(data, file, options = {}) {
|
|
|
207
208
|
if (response && !controller.signal.aborted)
|
|
208
209
|
options.response?.(response);
|
|
209
210
|
else
|
|
210
|
-
options.err?.('
|
|
211
|
+
options.err?.(t('reply.interactionEnded'));
|
|
211
212
|
}
|
|
212
213
|
catch (error) {
|
|
213
214
|
await stateTransaction(access, state => {
|
|
214
215
|
const current = state.requests[request.id];
|
|
215
216
|
if (current)
|
|
216
|
-
finishInteraction(current, { kind: current.claimed ? 'unknown' : 'not-applied', text: '
|
|
217
|
+
finishInteraction(current, { kind: current.claimed ? 'unknown' : 'not-applied', reason: 'failed', text: t('reply.interactionFailed') });
|
|
217
218
|
}).catch(() => { });
|
|
218
219
|
throw error;
|
|
219
220
|
}
|
|
@@ -224,7 +225,7 @@ export async function interactionHook(data, file, options = {}) {
|
|
|
224
225
|
await stateTransaction(access, state => {
|
|
225
226
|
const current = state.requests[request.id];
|
|
226
227
|
if (current)
|
|
227
|
-
finishInteraction(current, { kind: current.claimed ? 'unknown' : 'not-applied', text: '
|
|
228
|
+
finishInteraction(current, { kind: current.claimed ? 'unknown' : 'not-applied', reason: 'failed', text: t('reply.interactionCancelled') });
|
|
228
229
|
}).catch(() => { });
|
|
229
230
|
}
|
|
230
231
|
}
|