@foxden-app/foxclaw 0.4.13 → 0.4.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/.env.example +12 -2
- package/CHANGELOG.md +24 -0
- package/README.md +3 -1
- package/README_EN.md +3 -1
- package/dist/auth/cross_node_sync.d.ts +112 -0
- package/dist/auth/cross_node_sync.js +682 -0
- package/dist/auth/mirror.d.ts +37 -1
- package/dist/auth/mirror.js +136 -3
- package/dist/config.d.ts +10 -0
- package/dist/config.js +14 -0
- package/dist/controller/controller.d.ts +20 -0
- package/dist/controller/controller.js +173 -4
- package/dist/i18n.d.ts +40 -2
- package/dist/i18n.js +40 -2
- package/dist/main.js +192 -8
- package/dist/telegram/api.d.ts +6 -0
- package/dist/telegram/api.js +50 -0
- package/dist/telegram/gateway.d.ts +9 -0
- package/dist/telegram/gateway.js +33 -2
- package/dist/types.d.ts +14 -0
- package/docs/user-manual.md +36 -0
- package/docs/zh/user-manual.md +36 -0
- package/package.json +1 -1
- package/skills/foxclaw/SKILL.md +15 -7
- package/skills/foxclaw/agents/openai.yaml +1 -1
- package/skills/npm-publish/SKILL.md +2 -0
package/dist/i18n.d.ts
CHANGED
|
@@ -125,6 +125,8 @@ declare const MESSAGES: {
|
|
|
125
125
|
readonly status_runtime_weixin: "- Weixin default runtime: connected {connected}, active turns {turns}";
|
|
126
126
|
readonly status_auth_mirror_none: "Last auth mirror: none recorded";
|
|
127
127
|
readonly status_auth_mirror_synced: "Last auth mirror: {candidate} from {source} at {time}";
|
|
128
|
+
readonly status_auth_sync: "Cross-node auth sync: node {node}, peers {peers}, pending imports {pending}";
|
|
129
|
+
readonly status_auth_sync_error: "Cross-node auth sync error: {value}";
|
|
128
130
|
readonly status_last_update_none: "Last service update: none recorded";
|
|
129
131
|
readonly status_last_update: "Last service update: {from} -> {to} at {time}";
|
|
130
132
|
readonly status_last_codex_update: "Last Codex update: {value}";
|
|
@@ -138,7 +140,8 @@ declare const MESSAGES: {
|
|
|
138
140
|
readonly auth_reload_restarting: "Restarting Codex app-server to reload auth...";
|
|
139
141
|
readonly auth_reload_done: "Codex app-server restarted. Current auth has been reloaded.";
|
|
140
142
|
readonly auth_reload_blocked_active: "Cannot reload Codex auth while a turn, approval, or question is active. Wait or use /interrupt first.";
|
|
141
|
-
readonly usage_auth: "Usage: /auth [list [keyword]|filter <all|enabled|attention>|page <n>|use <n>|enable <n>|disable <n>|reload|refresh all [confirm]|add <name>]";
|
|
143
|
+
readonly usage_auth: "Usage: /auth [list [keyword]|filter <all|enabled|attention>|page <n>|use <n>|enable <n>|disable <n>|reload|refresh all [confirm]|sync <status|test|push all>|add <name>]";
|
|
144
|
+
readonly usage_auth_sync: "Usage: /auth sync <status|test|push all>";
|
|
142
145
|
readonly usage_auth_add: "Usage: /auth add <name>. Use letters, numbers, dot, dash, or underscore.";
|
|
143
146
|
readonly auth_list_title: "Codex auth files:";
|
|
144
147
|
readonly auth_bot: "Bot runtime: {value}";
|
|
@@ -180,11 +183,13 @@ declare const MESSAGES: {
|
|
|
180
183
|
readonly auth_refresh_all_cancelled: "Refresh all cancelled.";
|
|
181
184
|
readonly auth_refresh_all_starting: "Refreshing all ChatGPT auth candidates. Every runtime must stay idle...";
|
|
182
185
|
readonly auth_refresh_all_blocked_active: "Cannot refresh all auth candidates while any runtime, approval, input, login, or auth mirror write is active. Wait or use /interrupt first.";
|
|
186
|
+
readonly auth_refresh_all_lease_failed: "Cross-node refresh lock was not granted: {error}";
|
|
183
187
|
readonly auth_refresh_all_done: "Auth refresh all complete: {refreshed} refreshed, {skipped} skipped, {failed} failed.";
|
|
184
188
|
readonly auth_refresh_all_refreshed: "Refreshed: {value}";
|
|
185
189
|
readonly auth_refresh_all_skipped: "Skipped non-ChatGPT/invalid candidates: {value}";
|
|
186
190
|
readonly auth_refresh_all_failed: "Failed: {value}";
|
|
187
191
|
readonly auth_auto_switching: "Codex auth problem detected ({error}). Switching: {from} -> {to}...";
|
|
192
|
+
readonly auth_auto_recovered_current: "Codex auth problem detected ({error}). Recovered a newer same-account credential for {value} and restarted Codex app-server.";
|
|
188
193
|
readonly auth_auto_done: "Auto-switched Codex auth: {from} -> {to}.";
|
|
189
194
|
readonly auth_auto_retrying: "Retrying the same request with the new auth...";
|
|
190
195
|
readonly auth_auto_retry_thread_missing: "Auth was switched, but the original thread is no longer available ({threadId}). Retry was stopped to avoid creating duplicate sessions.";
|
|
@@ -197,6 +202,20 @@ declare const MESSAGES: {
|
|
|
197
202
|
readonly auth_add_cancelled: "New auth login cancelled. Restored previous auth.";
|
|
198
203
|
readonly auth_add_reverted: "Restored previous auth.";
|
|
199
204
|
readonly auth_add_missing_file: "Login completed, but the new auth file was not created: {value}";
|
|
205
|
+
readonly auth_sync_disabled: "Cross-node auth sync is disabled.";
|
|
206
|
+
readonly auth_sync_test_sent: "Auth sync test ping sent to {count} peer(s).";
|
|
207
|
+
readonly auth_sync_push_blocked_active: "Cannot push auth sync while any runtime, approval, input, login, or auth mirror write is active.";
|
|
208
|
+
readonly auth_sync_push_done: "Auth sync push complete: sent {sent}, skipped {skipped}.";
|
|
209
|
+
readonly auth_sync_status_title: "Cross-node auth sync:";
|
|
210
|
+
readonly auth_sync_status_node: "Node: {value}";
|
|
211
|
+
readonly auth_sync_status_peers: "Peers: {value}";
|
|
212
|
+
readonly auth_sync_status_pending: "Pending imports: {value}";
|
|
213
|
+
readonly auth_sync_status_sent: "Last sent: {value}";
|
|
214
|
+
readonly auth_sync_status_received: "Last received: {value}";
|
|
215
|
+
readonly auth_sync_status_imported: "Last import: {value}";
|
|
216
|
+
readonly auth_sync_status_pull: "Last pull: {value}";
|
|
217
|
+
readonly auth_sync_status_lease: "Active refresh lease: {value}";
|
|
218
|
+
readonly auth_sync_status_error: "Last error: {value}";
|
|
200
219
|
readonly button_login_device: "🔑 Login";
|
|
201
220
|
readonly button_auth_reload: "🔄 Reload auth";
|
|
202
221
|
readonly button_auth_refresh_all_confirm: "⚠️ Accept risk & refresh";
|
|
@@ -731,6 +750,8 @@ declare const MESSAGES: {
|
|
|
731
750
|
readonly status_runtime_weixin: "- 微信默认运行时:连接 {connected},进行中回复 {turns}";
|
|
732
751
|
readonly status_auth_mirror_none: "最近 auth 镜像:暂无记录";
|
|
733
752
|
readonly status_auth_mirror_synced: "最近 auth 镜像:{candidate} 由 {source} 于 {time} 同步";
|
|
753
|
+
readonly status_auth_sync: "跨节点 auth 同步:节点 {node},peer {peers},待导入 {pending}";
|
|
754
|
+
readonly status_auth_sync_error: "跨节点 auth 同步错误:{value}";
|
|
734
755
|
readonly status_last_update_none: "最近服务升级:暂无记录";
|
|
735
756
|
readonly status_last_update: "最近服务升级:{from} -> {to}({time})";
|
|
736
757
|
readonly status_last_codex_update: "最近 Codex 升级:{value}";
|
|
@@ -744,7 +765,8 @@ declare const MESSAGES: {
|
|
|
744
765
|
readonly auth_reload_restarting: "正在重启 Codex app-server 以重新读取 auth...";
|
|
745
766
|
readonly auth_reload_done: "Codex app-server 已重启,当前 auth 已重新读取。";
|
|
746
767
|
readonly auth_reload_blocked_active: "当前有回复、审批或问题在进行中,不能重载 Codex auth。请先等待,或使用 /interrupt。";
|
|
747
|
-
readonly usage_auth: "用法:/auth [list [关键词]|filter <all|enabled|attention>|page <页码>|use <编号>|enable <编号>|disable <编号>|reload|refresh all [confirm]|add <名称>]";
|
|
768
|
+
readonly usage_auth: "用法:/auth [list [关键词]|filter <all|enabled|attention>|page <页码>|use <编号>|enable <编号>|disable <编号>|reload|refresh all [confirm]|sync <status|test|push all>|add <名称>]";
|
|
769
|
+
readonly usage_auth_sync: "用法:/auth sync <status|test|push all>";
|
|
748
770
|
readonly usage_auth_add: "用法:/auth add <名称>。名称只能包含字母、数字、点、短横线或下划线。";
|
|
749
771
|
readonly auth_list_title: "Codex auth 文件:";
|
|
750
772
|
readonly auth_bot: "Bot runtime:{value}";
|
|
@@ -786,11 +808,13 @@ declare const MESSAGES: {
|
|
|
786
808
|
readonly auth_refresh_all_cancelled: "已取消刷新全部。";
|
|
787
809
|
readonly auth_refresh_all_starting: "正在刷新全部 ChatGPT auth 候选。执行期间所有 runtime 必须保持空闲...";
|
|
788
810
|
readonly auth_refresh_all_blocked_active: "当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能刷新全部 auth。请先等待,或使用 /interrupt。";
|
|
811
|
+
readonly auth_refresh_all_lease_failed: "跨节点刷新锁未授予:{error}";
|
|
789
812
|
readonly auth_refresh_all_done: "全部 auth 刷新完成:已刷新 {refreshed},已跳过 {skipped},失败 {failed}。";
|
|
790
813
|
readonly auth_refresh_all_refreshed: "已刷新:{value}";
|
|
791
814
|
readonly auth_refresh_all_skipped: "已跳过非 ChatGPT/无效候选:{value}";
|
|
792
815
|
readonly auth_refresh_all_failed: "失败:{value}";
|
|
793
816
|
readonly auth_auto_switching: "检测到 Codex auth 问题({error}),正在切换:{from} -> {to}...";
|
|
817
|
+
readonly auth_auto_recovered_current: "检测到 Codex auth 问题({error}),已为 {value} 恢复同账号较新凭据并重启 Codex app-server。";
|
|
794
818
|
readonly auth_auto_done: "已自动切换 Codex auth:{from} -> {to}。";
|
|
795
819
|
readonly auth_auto_retrying: "正在用新的 auth 重试同一条请求...";
|
|
796
820
|
readonly auth_auto_retry_thread_missing: "Auth 已切换,但原线程已经不可用({threadId})。已停止重试,避免创建重复 session。";
|
|
@@ -803,6 +827,20 @@ declare const MESSAGES: {
|
|
|
803
827
|
readonly auth_add_cancelled: "新 auth 登录已取消,已恢复之前的 auth。";
|
|
804
828
|
readonly auth_add_reverted: "已恢复之前的 auth。";
|
|
805
829
|
readonly auth_add_missing_file: "登录已完成,但没有创建新的 auth 文件:{value}";
|
|
830
|
+
readonly auth_sync_disabled: "跨节点 auth 同步未启用。";
|
|
831
|
+
readonly auth_sync_test_sent: "已向 {count} 个 peer 发送 auth sync 测试 ping。";
|
|
832
|
+
readonly auth_sync_push_blocked_active: "当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能推送 auth 同步。";
|
|
833
|
+
readonly auth_sync_push_done: "auth 同步推送完成:已发送 {sent},已跳过 {skipped}。";
|
|
834
|
+
readonly auth_sync_status_title: "跨节点 auth 同步:";
|
|
835
|
+
readonly auth_sync_status_node: "节点:{value}";
|
|
836
|
+
readonly auth_sync_status_peers: "Peer:{value}";
|
|
837
|
+
readonly auth_sync_status_pending: "待导入:{value}";
|
|
838
|
+
readonly auth_sync_status_sent: "最近发送:{value}";
|
|
839
|
+
readonly auth_sync_status_received: "最近接收:{value}";
|
|
840
|
+
readonly auth_sync_status_imported: "最近导入:{value}";
|
|
841
|
+
readonly auth_sync_status_pull: "最近拉取:{value}";
|
|
842
|
+
readonly auth_sync_status_lease: "当前刷新锁:{value}";
|
|
843
|
+
readonly auth_sync_status_error: "最近错误:{value}";
|
|
806
844
|
readonly button_login_device: "🔑 设备登录";
|
|
807
845
|
readonly button_auth_reload: "🔄 重载 auth";
|
|
808
846
|
readonly button_auth_refresh_all_confirm: "⚠️ 接受风险并刷新";
|
package/dist/i18n.js
CHANGED
|
@@ -123,6 +123,8 @@ const MESSAGES = {
|
|
|
123
123
|
status_runtime_weixin: '- Weixin default runtime: connected {connected}, active turns {turns}',
|
|
124
124
|
status_auth_mirror_none: 'Last auth mirror: none recorded',
|
|
125
125
|
status_auth_mirror_synced: 'Last auth mirror: {candidate} from {source} at {time}',
|
|
126
|
+
status_auth_sync: 'Cross-node auth sync: node {node}, peers {peers}, pending imports {pending}',
|
|
127
|
+
status_auth_sync_error: 'Cross-node auth sync error: {value}',
|
|
126
128
|
status_last_update_none: 'Last service update: none recorded',
|
|
127
129
|
status_last_update: 'Last service update: {from} -> {to} at {time}',
|
|
128
130
|
status_last_codex_update: 'Last Codex update: {value}',
|
|
@@ -136,7 +138,8 @@ const MESSAGES = {
|
|
|
136
138
|
auth_reload_restarting: 'Restarting Codex app-server to reload auth...',
|
|
137
139
|
auth_reload_done: 'Codex app-server restarted. Current auth has been reloaded.',
|
|
138
140
|
auth_reload_blocked_active: 'Cannot reload Codex auth while a turn, approval, or question is active. Wait or use /interrupt first.',
|
|
139
|
-
usage_auth: 'Usage: /auth [list [keyword]|filter <all|enabled|attention>|page <n>|use <n>|enable <n>|disable <n>|reload|refresh all [confirm]|add <name>]',
|
|
141
|
+
usage_auth: 'Usage: /auth [list [keyword]|filter <all|enabled|attention>|page <n>|use <n>|enable <n>|disable <n>|reload|refresh all [confirm]|sync <status|test|push all>|add <name>]',
|
|
142
|
+
usage_auth_sync: 'Usage: /auth sync <status|test|push all>',
|
|
140
143
|
usage_auth_add: 'Usage: /auth add <name>. Use letters, numbers, dot, dash, or underscore.',
|
|
141
144
|
auth_list_title: 'Codex auth files:',
|
|
142
145
|
auth_bot: 'Bot runtime: {value}',
|
|
@@ -178,11 +181,13 @@ const MESSAGES = {
|
|
|
178
181
|
auth_refresh_all_cancelled: 'Refresh all cancelled.',
|
|
179
182
|
auth_refresh_all_starting: 'Refreshing all ChatGPT auth candidates. Every runtime must stay idle...',
|
|
180
183
|
auth_refresh_all_blocked_active: 'Cannot refresh all auth candidates while any runtime, approval, input, login, or auth mirror write is active. Wait or use /interrupt first.',
|
|
184
|
+
auth_refresh_all_lease_failed: 'Cross-node refresh lock was not granted: {error}',
|
|
181
185
|
auth_refresh_all_done: 'Auth refresh all complete: {refreshed} refreshed, {skipped} skipped, {failed} failed.',
|
|
182
186
|
auth_refresh_all_refreshed: 'Refreshed: {value}',
|
|
183
187
|
auth_refresh_all_skipped: 'Skipped non-ChatGPT/invalid candidates: {value}',
|
|
184
188
|
auth_refresh_all_failed: 'Failed: {value}',
|
|
185
189
|
auth_auto_switching: 'Codex auth problem detected ({error}). Switching: {from} -> {to}...',
|
|
190
|
+
auth_auto_recovered_current: 'Codex auth problem detected ({error}). Recovered a newer same-account credential for {value} and restarted Codex app-server.',
|
|
186
191
|
auth_auto_done: 'Auto-switched Codex auth: {from} -> {to}.',
|
|
187
192
|
auth_auto_retrying: 'Retrying the same request with the new auth...',
|
|
188
193
|
auth_auto_retry_thread_missing: 'Auth was switched, but the original thread is no longer available ({threadId}). Retry was stopped to avoid creating duplicate sessions.',
|
|
@@ -195,6 +200,20 @@ const MESSAGES = {
|
|
|
195
200
|
auth_add_cancelled: 'New auth login cancelled. Restored previous auth.',
|
|
196
201
|
auth_add_reverted: 'Restored previous auth.',
|
|
197
202
|
auth_add_missing_file: 'Login completed, but the new auth file was not created: {value}',
|
|
203
|
+
auth_sync_disabled: 'Cross-node auth sync is disabled.',
|
|
204
|
+
auth_sync_test_sent: 'Auth sync test ping sent to {count} peer(s).',
|
|
205
|
+
auth_sync_push_blocked_active: 'Cannot push auth sync while any runtime, approval, input, login, or auth mirror write is active.',
|
|
206
|
+
auth_sync_push_done: 'Auth sync push complete: sent {sent}, skipped {skipped}.',
|
|
207
|
+
auth_sync_status_title: 'Cross-node auth sync:',
|
|
208
|
+
auth_sync_status_node: 'Node: {value}',
|
|
209
|
+
auth_sync_status_peers: 'Peers: {value}',
|
|
210
|
+
auth_sync_status_pending: 'Pending imports: {value}',
|
|
211
|
+
auth_sync_status_sent: 'Last sent: {value}',
|
|
212
|
+
auth_sync_status_received: 'Last received: {value}',
|
|
213
|
+
auth_sync_status_imported: 'Last import: {value}',
|
|
214
|
+
auth_sync_status_pull: 'Last pull: {value}',
|
|
215
|
+
auth_sync_status_lease: 'Active refresh lease: {value}',
|
|
216
|
+
auth_sync_status_error: 'Last error: {value}',
|
|
198
217
|
button_login_device: '🔑 Login',
|
|
199
218
|
button_auth_reload: '🔄 Reload auth',
|
|
200
219
|
button_auth_refresh_all_confirm: '⚠️ Accept risk & refresh',
|
|
@@ -729,6 +748,8 @@ const MESSAGES = {
|
|
|
729
748
|
status_runtime_weixin: '- 微信默认运行时:连接 {connected},进行中回复 {turns}',
|
|
730
749
|
status_auth_mirror_none: '最近 auth 镜像:暂无记录',
|
|
731
750
|
status_auth_mirror_synced: '最近 auth 镜像:{candidate} 由 {source} 于 {time} 同步',
|
|
751
|
+
status_auth_sync: '跨节点 auth 同步:节点 {node},peer {peers},待导入 {pending}',
|
|
752
|
+
status_auth_sync_error: '跨节点 auth 同步错误:{value}',
|
|
732
753
|
status_last_update_none: '最近服务升级:暂无记录',
|
|
733
754
|
status_last_update: '最近服务升级:{from} -> {to}({time})',
|
|
734
755
|
status_last_codex_update: '最近 Codex 升级:{value}',
|
|
@@ -742,7 +763,8 @@ const MESSAGES = {
|
|
|
742
763
|
auth_reload_restarting: '正在重启 Codex app-server 以重新读取 auth...',
|
|
743
764
|
auth_reload_done: 'Codex app-server 已重启,当前 auth 已重新读取。',
|
|
744
765
|
auth_reload_blocked_active: '当前有回复、审批或问题在进行中,不能重载 Codex auth。请先等待,或使用 /interrupt。',
|
|
745
|
-
usage_auth: '用法:/auth [list [关键词]|filter <all|enabled|attention>|page <页码>|use <编号>|enable <编号>|disable <编号>|reload|refresh all [confirm]|add <名称>]',
|
|
766
|
+
usage_auth: '用法:/auth [list [关键词]|filter <all|enabled|attention>|page <页码>|use <编号>|enable <编号>|disable <编号>|reload|refresh all [confirm]|sync <status|test|push all>|add <名称>]',
|
|
767
|
+
usage_auth_sync: '用法:/auth sync <status|test|push all>',
|
|
746
768
|
usage_auth_add: '用法:/auth add <名称>。名称只能包含字母、数字、点、短横线或下划线。',
|
|
747
769
|
auth_list_title: 'Codex auth 文件:',
|
|
748
770
|
auth_bot: 'Bot runtime:{value}',
|
|
@@ -784,11 +806,13 @@ const MESSAGES = {
|
|
|
784
806
|
auth_refresh_all_cancelled: '已取消刷新全部。',
|
|
785
807
|
auth_refresh_all_starting: '正在刷新全部 ChatGPT auth 候选。执行期间所有 runtime 必须保持空闲...',
|
|
786
808
|
auth_refresh_all_blocked_active: '当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能刷新全部 auth。请先等待,或使用 /interrupt。',
|
|
809
|
+
auth_refresh_all_lease_failed: '跨节点刷新锁未授予:{error}',
|
|
787
810
|
auth_refresh_all_done: '全部 auth 刷新完成:已刷新 {refreshed},已跳过 {skipped},失败 {failed}。',
|
|
788
811
|
auth_refresh_all_refreshed: '已刷新:{value}',
|
|
789
812
|
auth_refresh_all_skipped: '已跳过非 ChatGPT/无效候选:{value}',
|
|
790
813
|
auth_refresh_all_failed: '失败:{value}',
|
|
791
814
|
auth_auto_switching: '检测到 Codex auth 问题({error}),正在切换:{from} -> {to}...',
|
|
815
|
+
auth_auto_recovered_current: '检测到 Codex auth 问题({error}),已为 {value} 恢复同账号较新凭据并重启 Codex app-server。',
|
|
792
816
|
auth_auto_done: '已自动切换 Codex auth:{from} -> {to}。',
|
|
793
817
|
auth_auto_retrying: '正在用新的 auth 重试同一条请求...',
|
|
794
818
|
auth_auto_retry_thread_missing: 'Auth 已切换,但原线程已经不可用({threadId})。已停止重试,避免创建重复 session。',
|
|
@@ -801,6 +825,20 @@ const MESSAGES = {
|
|
|
801
825
|
auth_add_cancelled: '新 auth 登录已取消,已恢复之前的 auth。',
|
|
802
826
|
auth_add_reverted: '已恢复之前的 auth。',
|
|
803
827
|
auth_add_missing_file: '登录已完成,但没有创建新的 auth 文件:{value}',
|
|
828
|
+
auth_sync_disabled: '跨节点 auth 同步未启用。',
|
|
829
|
+
auth_sync_test_sent: '已向 {count} 个 peer 发送 auth sync 测试 ping。',
|
|
830
|
+
auth_sync_push_blocked_active: '当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能推送 auth 同步。',
|
|
831
|
+
auth_sync_push_done: 'auth 同步推送完成:已发送 {sent},已跳过 {skipped}。',
|
|
832
|
+
auth_sync_status_title: '跨节点 auth 同步:',
|
|
833
|
+
auth_sync_status_node: '节点:{value}',
|
|
834
|
+
auth_sync_status_peers: 'Peer:{value}',
|
|
835
|
+
auth_sync_status_pending: '待导入:{value}',
|
|
836
|
+
auth_sync_status_sent: '最近发送:{value}',
|
|
837
|
+
auth_sync_status_received: '最近接收:{value}',
|
|
838
|
+
auth_sync_status_imported: '最近导入:{value}',
|
|
839
|
+
auth_sync_status_pull: '最近拉取:{value}',
|
|
840
|
+
auth_sync_status_lease: '当前刷新锁:{value}',
|
|
841
|
+
auth_sync_status_error: '最近错误:{value}',
|
|
804
842
|
button_login_device: '🔑 设备登录',
|
|
805
843
|
button_auth_reload: '🔄 重载 auth',
|
|
806
844
|
button_auth_refresh_all_confirm: '⚠️ 接受风险并刷新',
|
package/dist/main.js
CHANGED
|
@@ -141,7 +141,7 @@ Usage:
|
|
|
141
141
|
foxclaw --help`);
|
|
142
142
|
}
|
|
143
143
|
async function runServeCli() {
|
|
144
|
-
const [{ BridgeMessagingRouter }, { TelegramMessagingPort }, { WeixinChannelAdapter }, { WeixinMessagingPort }, { attachIlinkRuntimeFromBridgeLogger }, { loadWeixinAccount }, { Logger }, { BridgeStore }, { TelegramGateway }, { CodexAppClient }, { BridgeSessionCore }, { TelegramChannelAdapter }, { AuthCandidateMirror },] = await Promise.all([
|
|
144
|
+
const [{ BridgeMessagingRouter }, { TelegramMessagingPort }, { WeixinChannelAdapter }, { WeixinMessagingPort }, { attachIlinkRuntimeFromBridgeLogger }, { loadWeixinAccount }, { Logger }, { BridgeStore }, { TelegramGateway }, { CodexAppClient }, { BridgeSessionCore }, { TelegramChannelAdapter }, { AuthCandidateMirror }, { CrossNodeAuthSync },] = await Promise.all([
|
|
145
145
|
import('./channels/bridge_messaging_router.js'),
|
|
146
146
|
import('./channels/telegram/telegram_messaging_port.js'),
|
|
147
147
|
import('./channels/weixin/weixin_channel_adapter.js'),
|
|
@@ -155,6 +155,7 @@ async function runServeCli() {
|
|
|
155
155
|
import('./controller/controller.js'),
|
|
156
156
|
import('./channels/telegram/telegram_channel_adapter.js'),
|
|
157
157
|
import('./auth/mirror.js'),
|
|
158
|
+
import('./auth/cross_node_sync.js'),
|
|
158
159
|
]);
|
|
159
160
|
const config = loadConfig();
|
|
160
161
|
const logger = new Logger(config.logLevel, config.logPath);
|
|
@@ -166,6 +167,7 @@ async function runServeCli() {
|
|
|
166
167
|
let activeTelegramAdapters = [];
|
|
167
168
|
let managedApps = [];
|
|
168
169
|
let activeAuthMirror = null;
|
|
170
|
+
let activeAuthSync = null;
|
|
169
171
|
try {
|
|
170
172
|
store = new BridgeStore(config.storePath);
|
|
171
173
|
if (config.tgMultiBotMode) {
|
|
@@ -206,6 +208,7 @@ async function runServeCli() {
|
|
|
206
208
|
const app = new CodexAppClient(runtimeConfig.codexCliBin, runtimeConfig.codexAppLaunchCmd, runtimeConfig.codexAppAutolaunch, runtimeConfig.codexAppServerStatePath, runtimeConfig.codexAppServerLogPath, logger, childEnv, sharedDefaultRuntime ? [] : ['cli_auth_credentials_store="file"']);
|
|
207
209
|
seeds.push({ id, home, authDir, sharedDefaultRuntime, config: runtimeConfig, bot, app });
|
|
208
210
|
}
|
|
211
|
+
let authSync = null;
|
|
209
212
|
const mirror = new AuthCandidateMirror(canonicalAuthDir, seeds.map((runtime) => ({
|
|
210
213
|
id: runtime.id,
|
|
211
214
|
label: runtime.bot.username ? `@${runtime.bot.username}` : runtime.id,
|
|
@@ -217,9 +220,12 @@ async function runServeCli() {
|
|
|
217
220
|
await runtime.bot.sendMessage(chatId, message);
|
|
218
221
|
}
|
|
219
222
|
},
|
|
220
|
-
})), logger, path.join(APP_HOME, 'runtime', 'auth-mirror.json')
|
|
223
|
+
})), logger, path.join(APP_HOME, 'runtime', 'auth-mirror.json'), {
|
|
224
|
+
onSynced: async (event) => {
|
|
225
|
+
await authSync?.publishCandidate(event.record.candidateName);
|
|
226
|
+
},
|
|
227
|
+
});
|
|
221
228
|
await mirror.initialize();
|
|
222
|
-
mirror.start();
|
|
223
229
|
activeAuthMirror = mirror;
|
|
224
230
|
managedApps = seeds.map((runtime) => runtime.app);
|
|
225
231
|
const selfUpdater = createSelfUpdateRuntime({
|
|
@@ -271,15 +277,35 @@ async function runServeCli() {
|
|
|
271
277
|
},
|
|
272
278
|
} : {}),
|
|
273
279
|
authMirror: mirror.getStatus(),
|
|
280
|
+
authSync: authSync?.getStatus() ?? null,
|
|
274
281
|
lastUpdate: lastSelfUpdate,
|
|
275
282
|
});
|
|
276
283
|
};
|
|
284
|
+
const authSyncLocalIdle = () => runtimes.every((runtime) => runtime.core.isIdleForServiceUpdate())
|
|
285
|
+
&& (!activeWeixinCore || activeWeixinCore.isIdleForServiceUpdate())
|
|
286
|
+
&& mirror.isIdle();
|
|
277
287
|
const coordinator = {
|
|
278
|
-
canSelfUpdate: () =>
|
|
279
|
-
&& (!
|
|
280
|
-
&& mirror.isIdle(),
|
|
288
|
+
canSelfUpdate: () => authSyncLocalIdle()
|
|
289
|
+
&& (!authSync || authSync.isIdle()),
|
|
281
290
|
authCandidateUpdated: (runtimeId, candidateName) => mirror.syncRuntimeCandidate(runtimeId, candidateName).then(() => undefined),
|
|
282
|
-
recoverAuthCandidate:
|
|
291
|
+
recoverAuthCandidate: async (runtimeId, candidateName) => {
|
|
292
|
+
const local = await mirror.recoverRuntimeCandidate(runtimeId, candidateName);
|
|
293
|
+
if (local)
|
|
294
|
+
return true;
|
|
295
|
+
const current = await mirror.readRuntimeCandidate(runtimeId, candidateName)
|
|
296
|
+
?? await mirror.readNewestCandidate(candidateName);
|
|
297
|
+
return await authSync?.requestRecovery(candidateName, {
|
|
298
|
+
accountId: current?.accountId ?? null,
|
|
299
|
+
lastRefreshMs: current?.lastRefreshMs ?? null,
|
|
300
|
+
}) ?? false;
|
|
301
|
+
},
|
|
302
|
+
acquireAuthRefreshLease: (reason) => authSync?.acquireRefreshLease(reason)
|
|
303
|
+
?? Promise.resolve({ ok: true, leaseId: null }),
|
|
304
|
+
releaseAuthRefreshLease: (leaseId) => authSync?.releaseRefreshLease(leaseId)
|
|
305
|
+
?? Promise.resolve(),
|
|
306
|
+
getAuthSyncStatus: () => authSync?.getStatus() ?? null,
|
|
307
|
+
authSyncPushAll: () => authSync?.pushAll() ?? Promise.resolve({ sent: 0, skipped: 0 }),
|
|
308
|
+
authSyncTest: () => authSync?.testPeers() ?? Promise.resolve({ sent: 0 }),
|
|
283
309
|
statusUpdated: () => writeAggregateStatus(),
|
|
284
310
|
getServiceStatus: async () => ({
|
|
285
311
|
bots: await Promise.all(runtimes.map(async (runtime) => {
|
|
@@ -302,6 +328,7 @@ async function runServeCli() {
|
|
|
302
328
|
},
|
|
303
329
|
} : {}),
|
|
304
330
|
authMirror: mirror.getStatus(),
|
|
331
|
+
authSync: authSync?.getStatus() ?? null,
|
|
305
332
|
lastUpdate: lastSelfUpdate,
|
|
306
333
|
}),
|
|
307
334
|
selfUpdateCompleted: (status) => {
|
|
@@ -324,6 +351,33 @@ async function runServeCli() {
|
|
|
324
351
|
weixinAdapter = new WeixinChannelAdapter(activeWeixinCore, store, config, logger);
|
|
325
352
|
}
|
|
326
353
|
activeTelegramAdapters = runtimes.map((runtime) => runtime.telegram);
|
|
354
|
+
if (config.authSyncEnabled) {
|
|
355
|
+
authSync = new CrossNodeAuthSync(buildAuthSyncConfig(config), logger, {
|
|
356
|
+
send: async (peer, envelope) => {
|
|
357
|
+
await seeds[0].bot.sendDocument(peer, `foxclaw-auth-sync-${Date.now()}.json`, Buffer.from(envelope, 'utf8'), 'FOXCLAW_AUTH_SYNC_V1');
|
|
358
|
+
},
|
|
359
|
+
}, {
|
|
360
|
+
readLocalCandidate: (candidateName) => mirror.readNewestCandidate(candidateName),
|
|
361
|
+
listLocalCandidates: () => mirror.listNewestCandidates(),
|
|
362
|
+
validateCandidate: async (candidateName, raw, expectedAccountId) => {
|
|
363
|
+
if (!authSyncLocalIdle()) {
|
|
364
|
+
return { ok: false, reason: 'runtime is not idle' };
|
|
365
|
+
}
|
|
366
|
+
const runtime = runtimes.find((entry) => entry.core.isIdleForServiceUpdate()) ?? runtimes[0] ?? null;
|
|
367
|
+
if (!runtime) {
|
|
368
|
+
return { ok: false, reason: 'no validation runtime is available' };
|
|
369
|
+
}
|
|
370
|
+
return runtime.core.validateExternalCodexAuthCandidate(candidateName, raw, expectedAccountId);
|
|
371
|
+
},
|
|
372
|
+
importCandidate: (candidateName, raw, source) => mirror.importExternalCandidate(candidateName, raw, source),
|
|
373
|
+
isIdle: authSyncLocalIdle,
|
|
374
|
+
});
|
|
375
|
+
await authSync.initialize();
|
|
376
|
+
activeAuthSync = authSync;
|
|
377
|
+
attachTelegramAuthSync(seeds[0].bot, authSync, config, logger);
|
|
378
|
+
authSync.start();
|
|
379
|
+
}
|
|
380
|
+
mirror.start();
|
|
327
381
|
process.on('unhandledRejection', (error) => {
|
|
328
382
|
logger.error('process.unhandled_rejection', { error: serializeError(error) });
|
|
329
383
|
});
|
|
@@ -341,6 +395,7 @@ async function runServeCli() {
|
|
|
341
395
|
logger.info('bridge.started', { bots: runtimes.map((runtime) => runtime.id) });
|
|
342
396
|
const shutdown = async (signal) => {
|
|
343
397
|
logger.info('bridge.shutting_down', { signal });
|
|
398
|
+
authSync?.stop();
|
|
344
399
|
mirror.stop();
|
|
345
400
|
await weixinAdapter?.stop();
|
|
346
401
|
await activeWeixinCore?.stop();
|
|
@@ -372,7 +427,85 @@ async function runServeCli() {
|
|
|
372
427
|
logPath: path.join(APP_HOME, 'logs', 'update.log'),
|
|
373
428
|
codexCliBin: config.codexCliBin,
|
|
374
429
|
});
|
|
375
|
-
|
|
430
|
+
let singleAuthSync = null;
|
|
431
|
+
let singleMirror = null;
|
|
432
|
+
let core = null;
|
|
433
|
+
const singleAuthDir = config.codexAuthDir ?? config.codexHome ?? process.env.CODEX_AUTH_DIR ?? path.join(os.homedir(), '.codex');
|
|
434
|
+
const singleAuthSyncLocalIdle = () => Boolean(core?.isIdleForServiceUpdate())
|
|
435
|
+
&& (!singleMirror || singleMirror.isIdle());
|
|
436
|
+
const singleCoordinator = config.authSyncEnabled ? {
|
|
437
|
+
canSelfUpdate: () => singleAuthSyncLocalIdle()
|
|
438
|
+
&& (!singleAuthSync || singleAuthSync.isIdle()),
|
|
439
|
+
authCandidateUpdated: (runtimeId, candidateName) => singleMirror?.syncRuntimeCandidate(runtimeId, candidateName).then(() => undefined) ?? Promise.resolve(),
|
|
440
|
+
recoverAuthCandidate: async (runtimeId, candidateName) => {
|
|
441
|
+
const local = await singleMirror?.recoverRuntimeCandidate(runtimeId, candidateName) ?? null;
|
|
442
|
+
if (local)
|
|
443
|
+
return true;
|
|
444
|
+
const current = await singleMirror?.readRuntimeCandidate(runtimeId, candidateName)
|
|
445
|
+
?? await singleMirror?.readNewestCandidate(candidateName)
|
|
446
|
+
?? null;
|
|
447
|
+
return await singleAuthSync?.requestRecovery(candidateName, {
|
|
448
|
+
accountId: current?.accountId ?? null,
|
|
449
|
+
lastRefreshMs: current?.lastRefreshMs ?? null,
|
|
450
|
+
}) ?? false;
|
|
451
|
+
},
|
|
452
|
+
acquireAuthRefreshLease: (reason) => singleAuthSync?.acquireRefreshLease(reason)
|
|
453
|
+
?? Promise.resolve({ ok: true, leaseId: null }),
|
|
454
|
+
releaseAuthRefreshLease: (leaseId) => singleAuthSync?.releaseRefreshLease(leaseId)
|
|
455
|
+
?? Promise.resolve(),
|
|
456
|
+
getAuthSyncStatus: () => singleAuthSync?.getStatus() ?? null,
|
|
457
|
+
authSyncPushAll: () => singleAuthSync?.pushAll() ?? Promise.resolve({ sent: 0, skipped: 0 }),
|
|
458
|
+
authSyncTest: () => singleAuthSync?.testPeers() ?? Promise.resolve({ sent: 0 }),
|
|
459
|
+
statusUpdated: (status) => {
|
|
460
|
+
writeRuntimeStatus(config.statusPath, {
|
|
461
|
+
...status,
|
|
462
|
+
authMirror: singleMirror?.getStatus() ?? null,
|
|
463
|
+
authSync: singleAuthSync?.getStatus() ?? null,
|
|
464
|
+
});
|
|
465
|
+
},
|
|
466
|
+
} : null;
|
|
467
|
+
if (config.authSyncEnabled) {
|
|
468
|
+
singleMirror = new AuthCandidateMirror(singleAuthDir, [{
|
|
469
|
+
id: 'default',
|
|
470
|
+
label: bot.username ? `@${bot.username}` : 'default',
|
|
471
|
+
authDir: singleAuthDir,
|
|
472
|
+
validate: async (context) => validateRefreshedAuthCandidate({
|
|
473
|
+
id: 'default',
|
|
474
|
+
authDir: singleAuthDir,
|
|
475
|
+
app,
|
|
476
|
+
}, context.candidateName),
|
|
477
|
+
}], logger, path.join(APP_HOME, 'runtime', 'auth-mirror.json'), {
|
|
478
|
+
onSynced: async (event) => {
|
|
479
|
+
await singleAuthSync?.publishCandidate(event.record.candidateName);
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
await singleMirror.initialize();
|
|
483
|
+
activeAuthMirror = singleMirror;
|
|
484
|
+
}
|
|
485
|
+
core = new BridgeSessionCore(config, store, logger, bot, app, outbound, selfUpdater, singleCoordinator);
|
|
486
|
+
if (config.authSyncEnabled && singleMirror) {
|
|
487
|
+
singleAuthSync = new CrossNodeAuthSync(buildAuthSyncConfig(config), logger, {
|
|
488
|
+
send: async (peer, envelope) => {
|
|
489
|
+
await bot.sendDocument(peer, `foxclaw-auth-sync-${Date.now()}.json`, Buffer.from(envelope, 'utf8'), 'FOXCLAW_AUTH_SYNC_V1');
|
|
490
|
+
},
|
|
491
|
+
}, {
|
|
492
|
+
readLocalCandidate: (candidateName) => singleMirror.readNewestCandidate(candidateName),
|
|
493
|
+
listLocalCandidates: () => singleMirror.listNewestCandidates(),
|
|
494
|
+
validateCandidate: async (candidateName, raw, expectedAccountId) => {
|
|
495
|
+
if (!singleAuthSyncLocalIdle()) {
|
|
496
|
+
return { ok: false, reason: 'runtime is not idle' };
|
|
497
|
+
}
|
|
498
|
+
return core.validateExternalCodexAuthCandidate(candidateName, raw, expectedAccountId);
|
|
499
|
+
},
|
|
500
|
+
importCandidate: (candidateName, raw, source) => singleMirror.importExternalCandidate(candidateName, raw, source),
|
|
501
|
+
isIdle: singleAuthSyncLocalIdle,
|
|
502
|
+
});
|
|
503
|
+
await singleAuthSync.initialize();
|
|
504
|
+
activeAuthSync = singleAuthSync;
|
|
505
|
+
attachTelegramAuthSync(bot, singleAuthSync, config, logger);
|
|
506
|
+
singleAuthSync.start();
|
|
507
|
+
singleMirror.start();
|
|
508
|
+
}
|
|
376
509
|
const telegram = new TelegramChannelAdapter(core);
|
|
377
510
|
managedApps = [app];
|
|
378
511
|
activeTelegramAdapters = [telegram];
|
|
@@ -392,6 +525,8 @@ async function runServeCli() {
|
|
|
392
525
|
logger.info('bridge.started', core.getRuntimeStatus());
|
|
393
526
|
const shutdown = async (signal) => {
|
|
394
527
|
logger.info('bridge.shutting_down', { signal });
|
|
528
|
+
singleAuthSync?.stop();
|
|
529
|
+
singleMirror?.stop();
|
|
395
530
|
await weixinAdapter?.stop();
|
|
396
531
|
await telegram.stop();
|
|
397
532
|
writeRuntimeStatus(config.statusPath, {
|
|
@@ -419,6 +554,7 @@ async function runServeCli() {
|
|
|
419
554
|
process.on('SIGTERM', () => void shutdown('SIGTERM'));
|
|
420
555
|
}
|
|
421
556
|
catch (error) {
|
|
557
|
+
activeAuthSync?.stop();
|
|
422
558
|
activeAuthMirror?.stop();
|
|
423
559
|
await weixinAdapter?.stop().catch(() => { });
|
|
424
560
|
await activeWeixinCore?.stop().catch(() => { });
|
|
@@ -429,6 +565,54 @@ async function runServeCli() {
|
|
|
429
565
|
throw error;
|
|
430
566
|
}
|
|
431
567
|
}
|
|
568
|
+
const AUTH_SYNC_TELEGRAM_CAPTION = 'FOXCLAW_AUTH_SYNC_V1';
|
|
569
|
+
function buildAuthSyncConfig(config) {
|
|
570
|
+
return {
|
|
571
|
+
enabled: config.authSyncEnabled,
|
|
572
|
+
transport: config.authSyncTransport,
|
|
573
|
+
key: config.authSyncKey,
|
|
574
|
+
peers: config.authSyncPeers,
|
|
575
|
+
nodeId: config.authSyncNodeId,
|
|
576
|
+
clusterId: config.authSyncClusterId,
|
|
577
|
+
statePath: config.authSyncStatePath,
|
|
578
|
+
tempDir: config.authSyncTempDir,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
function attachTelegramAuthSync(bot, sync, config, logger) {
|
|
582
|
+
bot.on('peerDocument', (event) => {
|
|
583
|
+
void handleTelegramAuthSyncDocument(bot, sync, config, logger, event).catch((error) => {
|
|
584
|
+
logger.warn('auth.sync.telegram_document_failed', { error: serializeError(error) });
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
async function handleTelegramAuthSyncDocument(bot, sync, config, logger, event) {
|
|
589
|
+
const fileName = event.attachment.fileName ?? '';
|
|
590
|
+
if (event.text.trim() !== AUTH_SYNC_TELEGRAM_CAPTION && !fileName.startsWith('foxclaw-auth-sync-')) {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const remoteFile = await bot.getFile(event.attachment.fileId);
|
|
594
|
+
if (!remoteFile.file_path) {
|
|
595
|
+
throw new Error('Telegram did not return file_path for auth sync document');
|
|
596
|
+
}
|
|
597
|
+
await fs.promises.mkdir(config.authSyncTempDir, { recursive: true, mode: 0o700 });
|
|
598
|
+
const destination = path.join(config.authSyncTempDir, `inbound-${process.pid}-${Date.now()}-${event.messageId}.json`);
|
|
599
|
+
try {
|
|
600
|
+
await bot.downloadResolvedFile(remoteFile.file_path, destination);
|
|
601
|
+
const raw = await fs.promises.readFile(destination, 'utf8');
|
|
602
|
+
const handled = await sync.handleIncomingEnvelope(raw, {
|
|
603
|
+
userId: event.userId,
|
|
604
|
+
username: event.username,
|
|
605
|
+
});
|
|
606
|
+
if (handled) {
|
|
607
|
+
logger.info('auth.sync.telegram_document_handled', {
|
|
608
|
+
peer: event.username ? `@${event.username}` : event.userId,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
finally {
|
|
613
|
+
await fs.promises.rm(destination, { force: true }).catch(() => undefined);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
432
616
|
async function validateRefreshedAuthCandidate(runtime, candidateName) {
|
|
433
617
|
const authPath = path.join(runtime.authDir, 'auth.json');
|
|
434
618
|
const candidatePath = path.join(runtime.authDir, candidateName);
|
package/dist/telegram/api.d.ts
CHANGED
|
@@ -10,5 +10,11 @@ export interface TelegramRemoteFile {
|
|
|
10
10
|
file_path?: string;
|
|
11
11
|
}
|
|
12
12
|
export declare function callTelegramApi<T>(botToken: string, method: string, body: Record<string, unknown>): Promise<TelegramApiResult<T>>;
|
|
13
|
+
export declare function callTelegramMultipartApi<T>(botToken: string, method: string, fields: Record<string, string>, files: Array<{
|
|
14
|
+
fieldName: string;
|
|
15
|
+
filename: string;
|
|
16
|
+
contents: Buffer;
|
|
17
|
+
contentType: string;
|
|
18
|
+
}>): Promise<TelegramApiResult<T>>;
|
|
13
19
|
export declare function getTelegramFile(botToken: string, fileId: string): Promise<TelegramRemoteFile>;
|
|
14
20
|
export declare function downloadTelegramFile(botToken: string, remoteFilePath: string, destinationPath: string): Promise<number>;
|
package/dist/telegram/api.js
CHANGED
|
@@ -39,6 +39,53 @@ export async function callTelegramApi(botToken, method, body) {
|
|
|
39
39
|
request.end();
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
|
+
export async function callTelegramMultipartApi(botToken, method, fields, files) {
|
|
43
|
+
const boundary = `foxclaw-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
44
|
+
const parts = [];
|
|
45
|
+
for (const [name, value] of Object.entries(fields)) {
|
|
46
|
+
parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="${escapeMultipartName(name)}"\r\n\r\n${value}\r\n`, 'utf8'));
|
|
47
|
+
}
|
|
48
|
+
for (const file of files) {
|
|
49
|
+
parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="${escapeMultipartName(file.fieldName)}"; filename="${escapeMultipartName(file.filename)}"\r\nContent-Type: ${file.contentType}\r\n\r\n`, 'utf8'));
|
|
50
|
+
parts.push(file.contents);
|
|
51
|
+
parts.push(Buffer.from('\r\n', 'utf8'));
|
|
52
|
+
}
|
|
53
|
+
parts.push(Buffer.from(`--${boundary}--\r\n`, 'utf8'));
|
|
54
|
+
const payload = Buffer.concat(parts);
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const request = https.request({
|
|
57
|
+
host: API_HOST,
|
|
58
|
+
port: 443,
|
|
59
|
+
path: `/bot${botToken}/${method}`,
|
|
60
|
+
method: 'POST',
|
|
61
|
+
family: 4,
|
|
62
|
+
headers: {
|
|
63
|
+
'content-type': `multipart/form-data; boundary=${boundary}`,
|
|
64
|
+
'content-length': payload.length,
|
|
65
|
+
},
|
|
66
|
+
}, (response) => {
|
|
67
|
+
const chunks = [];
|
|
68
|
+
response.on('data', (chunk) => {
|
|
69
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
70
|
+
});
|
|
71
|
+
response.on('end', () => {
|
|
72
|
+
try {
|
|
73
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
74
|
+
resolve(JSON.parse(text));
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
reject(new Error(`Failed to parse Telegram response: ${String(error)}`));
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
request.on('error', reject);
|
|
82
|
+
request.setTimeout(20_000, () => {
|
|
83
|
+
request.destroy(new Error(`Telegram API request timed out for ${method}`));
|
|
84
|
+
});
|
|
85
|
+
request.write(payload);
|
|
86
|
+
request.end();
|
|
87
|
+
});
|
|
88
|
+
}
|
|
42
89
|
export async function getTelegramFile(botToken, fileId) {
|
|
43
90
|
const result = await callTelegramApi(botToken, 'getFile', { file_id: fileId });
|
|
44
91
|
if (!result.ok || !result.result) {
|
|
@@ -87,3 +134,6 @@ export async function downloadTelegramFile(botToken, remoteFilePath, destination
|
|
|
87
134
|
response?.destroy();
|
|
88
135
|
}
|
|
89
136
|
}
|
|
137
|
+
function escapeMultipartName(value) {
|
|
138
|
+
return value.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('\r', '').replaceAll('\n', '');
|
|
139
|
+
}
|
|
@@ -17,6 +17,14 @@ export interface TelegramTextEvent {
|
|
|
17
17
|
replyToBot: boolean;
|
|
18
18
|
languageCode?: string;
|
|
19
19
|
}
|
|
20
|
+
export interface TelegramPeerDocumentEvent {
|
|
21
|
+
chatId: string;
|
|
22
|
+
userId: string;
|
|
23
|
+
username: string | null;
|
|
24
|
+
messageId: number;
|
|
25
|
+
text: string;
|
|
26
|
+
attachment: TelegramInboundAttachment;
|
|
27
|
+
}
|
|
20
28
|
export interface TelegramCallbackEvent {
|
|
21
29
|
chatId: string;
|
|
22
30
|
topicId: number | null;
|
|
@@ -53,6 +61,7 @@ export declare class TelegramGateway extends EventEmitter {
|
|
|
53
61
|
text: string;
|
|
54
62
|
callback_data: string;
|
|
55
63
|
}>>, messageThreadId?: number | null): Promise<number>;
|
|
64
|
+
sendDocument(chatId: string, filename: string, contents: Buffer, caption?: string): Promise<number>;
|
|
56
65
|
sendMessageDraft(chatId: string, draftId: number, text: string, messageThreadId?: number | null): Promise<void>;
|
|
57
66
|
editMessage(chatId: string, messageId: number, text: string, inlineKeyboard?: Array<Array<{
|
|
58
67
|
text: string;
|