auvezy-terminal-remote 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +17 -0
- package/README.md +107 -0
- package/dist/cli.js +4800 -0
- package/dist/postinstall.mjs +134 -0
- package/frontend-dist/assets/Geist-Variable-CrgPqtmy.woff2 +0 -0
- package/frontend-dist/assets/GeistMono-Variable-BNLlm6Cd.woff2 +0 -0
- package/frontend-dist/assets/index-BALTbT9e.js +325 -0
- package/frontend-dist/assets/index-DUpRzupd.css +32 -0
- package/frontend-dist/icons/atr-icon-180.png +0 -0
- package/frontend-dist/icons/atr-icon-192.png +0 -0
- package/frontend-dist/icons/atr-icon-512-maskable.png +0 -0
- package/frontend-dist/icons/atr-icon-512.png +0 -0
- package/frontend-dist/icons/atr-icon.svg +64 -0
- package/frontend-dist/index.html +35 -0
- package/frontend-dist/manifest.webmanifest +55 -0
- package/frontend-dist/screenshots/desktop.png +0 -0
- package/frontend-dist/screenshots/mobile.png +0 -0
- package/frontend-dist/sw.js +2 -0
- package/package.json +68 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,4800 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// shared/dist/constants.js
|
|
13
|
+
var DEFAULT_PORT, DEFAULT_SESSION_TTL_MS, DEFAULT_AUTH_RATE_LIMIT, DEFAULT_MAX_BUFFER_LINES, DEFAULT_SPAWN_TIMEOUT_SEC, TOKEN_BYTES, SESSION_ID_BYTES, WS_HEARTBEAT_INTERVAL_MS, MAX_WS_MESSAGE_SIZE, ATR_DATA_DIR, CONFIG_FILENAME, REGISTRY_FILENAME, SETTINGS_DIRNAME, VAPID_KEYS_FILENAME, PUSH_SUBSCRIPTIONS_FILENAME;
|
|
14
|
+
var init_constants = __esm({
|
|
15
|
+
"shared/dist/constants.js"() {
|
|
16
|
+
"use strict";
|
|
17
|
+
DEFAULT_PORT = 3e3;
|
|
18
|
+
DEFAULT_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
19
|
+
DEFAULT_AUTH_RATE_LIMIT = 20;
|
|
20
|
+
DEFAULT_MAX_BUFFER_LINES = 1e4;
|
|
21
|
+
DEFAULT_SPAWN_TIMEOUT_SEC = 30;
|
|
22
|
+
TOKEN_BYTES = 32;
|
|
23
|
+
SESSION_ID_BYTES = 32;
|
|
24
|
+
WS_HEARTBEAT_INTERVAL_MS = 3e4;
|
|
25
|
+
MAX_WS_MESSAGE_SIZE = 1024 * 1024;
|
|
26
|
+
ATR_DATA_DIR = ".auvezy/terminal-remote";
|
|
27
|
+
CONFIG_FILENAME = "config.json";
|
|
28
|
+
REGISTRY_FILENAME = "instances.json";
|
|
29
|
+
SETTINGS_DIRNAME = "settings";
|
|
30
|
+
VAPID_KEYS_FILENAME = "vapid-keys.json";
|
|
31
|
+
PUSH_SUBSCRIPTIONS_FILENAME = "push-subscriptions.json";
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// shared/dist/ws-protocol.js
|
|
36
|
+
function isServerMessage(value) {
|
|
37
|
+
if (!value || typeof value !== "object")
|
|
38
|
+
return false;
|
|
39
|
+
const type = value.type;
|
|
40
|
+
return type === "terminal_output" || type === "status_update" || type === "history_sync" || type === "heartbeat" || type === "error" || type === "session_ended" || type === "terminal_resize" || type === "ip_changed";
|
|
41
|
+
}
|
|
42
|
+
var init_ws_protocol = __esm({
|
|
43
|
+
"shared/dist/ws-protocol.js"() {
|
|
44
|
+
"use strict";
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// shared/dist/instance.js
|
|
49
|
+
var init_instance = __esm({
|
|
50
|
+
"shared/dist/instance.js"() {
|
|
51
|
+
"use strict";
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// shared/dist/defaults.js
|
|
56
|
+
function ensureDefaultUserConfig(input) {
|
|
57
|
+
const src = input ?? {};
|
|
58
|
+
const userShortcuts = Array.isArray(src.shortcuts) && src.shortcuts.length > 0 ? src.shortcuts : null;
|
|
59
|
+
const shortcutsLegacy = userShortcuts !== null && userShortcuts.some((s) => typeof s.group !== "string" || s.group.length === 0);
|
|
60
|
+
const shortcuts = userShortcuts === null || shortcutsLegacy ? DEFAULT_SHORTCUTS : userShortcuts;
|
|
61
|
+
const userCommands = Array.isArray(src.commands) && src.commands.length > 0 ? src.commands : null;
|
|
62
|
+
const commandsLegacy = userCommands !== null && userCommands.some((c) => typeof c.group !== "string" || c.group.length === 0);
|
|
63
|
+
const commands = userCommands === null || commandsLegacy ? DEFAULT_COMMANDS : userCommands;
|
|
64
|
+
return { ...src, shortcuts, commands };
|
|
65
|
+
}
|
|
66
|
+
var SHORTCUT_GROUPS, DEFAULT_SHORTCUTS, COMMAND_GROUPS, DEFAULT_COMMANDS;
|
|
67
|
+
var init_defaults = __esm({
|
|
68
|
+
"shared/dist/defaults.js"() {
|
|
69
|
+
"use strict";
|
|
70
|
+
SHORTCUT_GROUPS = [
|
|
71
|
+
{
|
|
72
|
+
id: "common",
|
|
73
|
+
title: "\u5E38\u7528",
|
|
74
|
+
desc: "\u6700\u5E38\u7528\u7684\u5BFC\u822A\u4E0E\u63A7\u5236\u952E\uFF0C\u9ED8\u8BA4\u5168\u90E8\u542F\u7528\u3002",
|
|
75
|
+
items: [
|
|
76
|
+
{ label: "Esc", data: "\x1B", enabled: true, desc: "ESC \u952E / \u53D6\u6D88\u5F53\u524D\u64CD\u4F5C" },
|
|
77
|
+
{ label: "Enter", data: "\r", enabled: true, desc: "\u56DE\u8F66 / \u786E\u8BA4" },
|
|
78
|
+
{ label: "Tab", data: " ", enabled: true, desc: "Tab / \u81EA\u52A8\u8865\u5168" },
|
|
79
|
+
{ label: "BkSp", data: "\x7F", enabled: true, desc: "\u9000\u683C / \u5220\u9664\u5149\u6807\u524D\u4E00\u4E2A\u5B57\u7B26" },
|
|
80
|
+
{ label: "\u2191", data: "\x1B[A", enabled: true, desc: "\u4E0A\u7BAD\u5934 / \u5386\u53F2\u547D\u4EE4\u4E0A\u4E00\u6761" },
|
|
81
|
+
{ label: "\u2193", data: "\x1B[B", enabled: true, desc: "\u4E0B\u7BAD\u5934 / \u5386\u53F2\u547D\u4EE4\u4E0B\u4E00\u6761" },
|
|
82
|
+
{ label: "\u2190", data: "\x1B[D", enabled: true, desc: "\u5DE6\u7BAD\u5934" },
|
|
83
|
+
{ label: "\u2192", data: "\x1B[C", enabled: true, desc: "\u53F3\u7BAD\u5934" },
|
|
84
|
+
{
|
|
85
|
+
label: "S-Tab",
|
|
86
|
+
data: "\x1B[Z",
|
|
87
|
+
enabled: true,
|
|
88
|
+
desc: "Shift+Tab / \u53CD\u5411\u5207\u6362\uFF08Claude \u5BA1\u6279\u83DC\u5355\u4E0A\u4E00\u9879\u3001\u83DC\u5355\u8865\u5168\u53CD\u5411\u8F6E\u8BE2\uFF09"
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "editing",
|
|
94
|
+
title: "\u884C\u7F16\u8F91",
|
|
95
|
+
desc: "\u5F53\u524D\u884C\u7F16\u8F91\u63A7\u5236\uFF1A\u6E05\u884C\u3001\u9000\u683C\u3001\u5220\u9664\u3001\u7EC8\u6B62\u5F53\u524D\u547D\u4EE4\u7B49\u3002\u5728\u5927\u591A\u6570 shell\uFF08zsh/bash\uFF09\u548C REPL\uFF08python/node\uFF09\u4E2D\u90FD\u53EF\u7528\u3002",
|
|
96
|
+
items: [
|
|
97
|
+
{ label: "^C", data: "", enabled: false, desc: "Ctrl-C / \u4E2D\u65AD\u5F53\u524D\u547D\u4EE4" },
|
|
98
|
+
{ label: "^D", data: "", enabled: false, desc: "Ctrl-D / EOF\uFF08\u7A7A\u884C\u4E0B\u9000\u51FA shell\uFF09" },
|
|
99
|
+
{ label: "^L", data: "\f", enabled: false, desc: "Ctrl-L / \u6E05\u5C4F\uFF08\u4FDD\u7559\u5F53\u524D\u884C\uFF09" },
|
|
100
|
+
{ label: "^U", data: "", enabled: false, desc: "Ctrl-U / \u5220\u9664\u5149\u6807\u5230\u884C\u9996" },
|
|
101
|
+
{ label: "^K", data: "\v", enabled: false, desc: "Ctrl-K / \u5220\u9664\u5149\u6807\u5230\u884C\u5C3E" },
|
|
102
|
+
{ label: "^W", data: "", enabled: false, desc: "Ctrl-W / \u5220\u9664\u524D\u4E00\u4E2A\u5355\u8BCD" },
|
|
103
|
+
{ label: "^A", data: "", enabled: false, desc: "Ctrl-A / \u79FB\u52A8\u5230\u884C\u9996" },
|
|
104
|
+
{ label: "^E", data: "", enabled: false, desc: "Ctrl-E / \u79FB\u52A8\u5230\u884C\u5C3E" },
|
|
105
|
+
{ label: "^Z", data: "", enabled: false, desc: "Ctrl-Z / \u6302\u8D77\u5F53\u524D\u8FDB\u7A0B\u5230\u540E\u53F0" }
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: "readline",
|
|
110
|
+
title: "Readline \u7F16\u8F91",
|
|
111
|
+
desc: "GNU Readline / zsh emacs \u98CE\u683C\u7684\u8FDB\u9636\u7F16\u8F91\uFF1A\u6309\u5355\u8BCD\u8DF3\u8F6C\u3001\u64A4\u9500\u3001\u641C\u7D22\u5386\u53F2\u3002\u5728 bash/zsh/python REPL/psql \u7B49\u57FA\u4E8E readline \u7684\u7A0B\u5E8F\u91CC\u751F\u6548\u3002",
|
|
112
|
+
items: [
|
|
113
|
+
{ label: "\u2325\u2190", data: "\x1Bb", enabled: false, desc: "Alt-B / \u5411\u540E\u8DF3\u4E00\u4E2A\u5355\u8BCD" },
|
|
114
|
+
{ label: "\u2325\u2192", data: "\x1Bf", enabled: false, desc: "Alt-F / \u5411\u524D\u8DF3\u4E00\u4E2A\u5355\u8BCD" },
|
|
115
|
+
{ label: "^R", data: "", enabled: false, desc: "Ctrl-R / \u53CD\u5411\u641C\u7D22\u5386\u53F2\u547D\u4EE4" },
|
|
116
|
+
{ label: "^S", data: "", enabled: false, desc: "Ctrl-S / \u6B63\u5411\u641C\u7D22\u5386\u53F2\uFF08\u90E8\u5206\u7EC8\u7AEF\u88AB\u6D41\u63A7\u5360\u7528\uFF09" },
|
|
117
|
+
{ label: "^T", data: "", enabled: false, desc: "Ctrl-T / \u4EA4\u6362\u5149\u6807\u524D\u540E\u4E24\u4E2A\u5B57\u7B26" },
|
|
118
|
+
{ label: "^Y", data: "", enabled: false, desc: "Ctrl-Y / \u7C98\u8D34\uFF08yank\uFF09\u521A\u5220\u9664\u7684\u5185\u5BB9" },
|
|
119
|
+
{ label: "^_", data: "", enabled: false, desc: "Ctrl-_ / \u64A4\u9500\u4E0A\u4E00\u6B21\u7F16\u8F91" },
|
|
120
|
+
{ label: "\u2325D", data: "\x1Bd", enabled: false, desc: "Alt-D / \u5411\u524D\u5220\u4E00\u4E2A\u5355\u8BCD" },
|
|
121
|
+
{ label: "\u2325.", data: "\x1B.", enabled: false, desc: "Alt-. / \u63D2\u5165\u4E0A\u6761\u547D\u4EE4\u7684\u6700\u540E\u4E00\u4E2A\u53C2\u6570" }
|
|
122
|
+
]
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: "vim",
|
|
126
|
+
title: "Vim",
|
|
127
|
+
desc: "Vim / Neovim \u64CD\u4F5C\u3002\u5305\u542B Esc \u9000\u5230 Normal \u6A21\u5F0F\u3001\u4FDD\u5B58\u9000\u51FA\u3001\u641C\u7D22\u7B49\u3002\u4EC5\u5728\u4F60\u5E38\u7528 vim \u7F16\u8F91\u6587\u4EF6\u65F6\u542F\u7528\u3002",
|
|
128
|
+
items: [
|
|
129
|
+
{ label: ":w", data: ":w\r", enabled: false, desc: "\u4FDD\u5B58\uFF08Normal \u6A21\u5F0F\u4E0B\uFF09" },
|
|
130
|
+
{ label: ":q", data: ":q\r", enabled: false, desc: "\u9000\u51FA\uFF08Normal \u6A21\u5F0F\u4E0B\uFF09" },
|
|
131
|
+
{ label: ":wq", data: ":wq\r", enabled: false, desc: "\u4FDD\u5B58\u5E76\u9000\u51FA" },
|
|
132
|
+
{ label: ":q!", data: ":q!\r", enabled: false, desc: "\u5F3A\u5236\u9000\u51FA\u4E0D\u4FDD\u5B58" },
|
|
133
|
+
{ label: "gg", data: "gg", enabled: false, desc: "\u8DF3\u5230\u6587\u4EF6\u5F00\u5934\uFF08Normal \u6A21\u5F0F\uFF09" },
|
|
134
|
+
{ label: "G", data: "G", enabled: false, desc: "\u8DF3\u5230\u6587\u4EF6\u672B\u5C3E\uFF08Normal \u6A21\u5F0F\uFF09" },
|
|
135
|
+
{ label: "u", data: "u", enabled: false, desc: "\u64A4\u9500\uFF08Normal \u6A21\u5F0F\uFF09" },
|
|
136
|
+
{ label: "^R", data: "", enabled: false, desc: "Ctrl-R / \u91CD\u505A\uFF08Normal \u6A21\u5F0F\uFF09" },
|
|
137
|
+
{ label: "/", data: "/", enabled: false, desc: "\u8FDB\u5165\u641C\u7D22\u6A21\u5F0F" },
|
|
138
|
+
{ label: "n", data: "n", enabled: false, desc: "\u8DF3\u5230\u4E0B\u4E00\u4E2A\u641C\u7D22\u5339\u914D" }
|
|
139
|
+
]
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: "tmux",
|
|
143
|
+
title: "tmux / screen",
|
|
144
|
+
desc: "tmux\uFF08\u9ED8\u8BA4\u524D\u7F00 Ctrl-B\uFF09\u548C GNU screen\uFF08\u9ED8\u8BA4\u524D\u7F00 Ctrl-A\uFF09\u7684\u5E38\u7528\u64CD\u4F5C\u3002\u524D\u7F00\u952E\u53D1\u51FA\u540E\u518D\u6309\u76EE\u6807\u952E\u5373\u89E6\u53D1\u3002\u5982\u679C\u6539\u8FC7\u524D\u7F00\u952E\u8BF7\u81EA\u884C\u7F16\u8F91 data \u5B57\u6BB5\u3002",
|
|
145
|
+
items: [
|
|
146
|
+
{ label: "tm:c", data: "c", enabled: false, desc: "tmux \u65B0\u5EFA\u7A97\u53E3\uFF08prefix + c\uFF09" },
|
|
147
|
+
{ label: "tm:n", data: "n", enabled: false, desc: "tmux \u4E0B\u4E00\u4E2A\u7A97\u53E3\uFF08prefix + n\uFF09" },
|
|
148
|
+
{ label: "tm:p", data: "p", enabled: false, desc: "tmux \u4E0A\u4E00\u4E2A\u7A97\u53E3\uFF08prefix + p\uFF09" },
|
|
149
|
+
{ label: "tm:d", data: "d", enabled: false, desc: "tmux \u5206\u79BB\u4F1A\u8BDD\uFF08prefix + d\uFF09" },
|
|
150
|
+
{ label: "tm:%", data: "%", enabled: false, desc: "tmux \u5782\u76F4\u5206\u5C4F\uFF08prefix + %\uFF09" },
|
|
151
|
+
{ label: 'tm:"', data: '"', enabled: false, desc: 'tmux \u6C34\u5E73\u5206\u5C4F\uFF08prefix + "\uFF09' },
|
|
152
|
+
{ label: "tm:x", data: "x", enabled: false, desc: "tmux \u5173\u95ED\u5F53\u524D\u9762\u677F\uFF08prefix + x\uFF09" },
|
|
153
|
+
{ label: "sc:c", data: "c", enabled: false, desc: "screen \u65B0\u5EFA\u7A97\u53E3\uFF08prefix + c\uFF09" },
|
|
154
|
+
{ label: "sc:n", data: "n", enabled: false, desc: "screen \u4E0B\u4E00\u4E2A\u7A97\u53E3\uFF08prefix + n\uFF09" },
|
|
155
|
+
{ label: "sc:d", data: "d", enabled: false, desc: "screen \u5206\u79BB\u4F1A\u8BDD\uFF08prefix + d\uFF09" }
|
|
156
|
+
]
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: "signals",
|
|
160
|
+
title: "\u8FDB\u7A0B\u4FE1\u53F7",
|
|
161
|
+
desc: "\u76F4\u63A5\u53D1\u9001\u8FDB\u7A0B\u63A7\u5236\u5B57\u7B26\u3002\u4E0E\u300C\u884C\u7F16\u8F91\u300D\u7EC4\u91CC\u7684 ^C/^D/^Z \u7269\u7406\u6309\u952E\u76F8\u540C\uFF0C\u53EA\u662F\u6309\u7528\u9014\u5355\u72EC\u5217\u51FA\uFF0C\u65B9\u4FBF\u624B\u673A\u4E0A\u4E00\u952E\u505C / \u9000 / \u6302\u8D77\u8FDB\u7A0B\u3002",
|
|
162
|
+
items: [
|
|
163
|
+
{ label: "SIGINT", data: "", enabled: false, desc: "Ctrl-C / \u4E2D\u65AD\uFF08INT \u4FE1\u53F7\uFF09" },
|
|
164
|
+
{ label: "EOF", data: "", enabled: false, desc: "Ctrl-D / \u53D1\u9001 EOF\uFF08\u5173\u95ED\u8F93\u5165\uFF09" },
|
|
165
|
+
{ label: "SIGTSTP", data: "", enabled: false, desc: "Ctrl-Z / \u6302\u8D77\u5230\u540E\u53F0\uFF08TSTP \u4FE1\u53F7\uFF09" },
|
|
166
|
+
{ label: "SIGQUIT", data: "", enabled: false, desc: "Ctrl-\\ / \u9000\u51FA\u5E76 core dump\uFF08QUIT \u4FE1\u53F7\uFF09" }
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
];
|
|
170
|
+
DEFAULT_SHORTCUTS = SHORTCUT_GROUPS.flatMap((g) => g.items.map((s) => ({ ...s, group: g.id })));
|
|
171
|
+
COMMAND_GROUPS = [
|
|
172
|
+
{
|
|
173
|
+
id: "session",
|
|
174
|
+
title: "\u4F1A\u8BDD",
|
|
175
|
+
desc: "\u4F1A\u8BDD\u751F\u547D\u5468\u671F\u76F8\u5173\uFF1A\u6E05\u7A7A\u3001\u538B\u7F29\u3001\u6062\u590D\u3001\u9000\u51FA\u3002",
|
|
176
|
+
items: [
|
|
177
|
+
{ label: "/clear", command: "/clear", enabled: true, autoSend: true, desc: "\u6E05\u7A7A\u5F53\u524D\u4F1A\u8BDD\u5386\u53F2" },
|
|
178
|
+
{ label: "/compact", command: "/compact", enabled: true, autoSend: true, desc: "\u5C06\u4F1A\u8BDD\u538B\u7F29\u6210\u6458\u8981\u4EE5\u8282\u7701\u4E0A\u4E0B\u6587" },
|
|
179
|
+
{ label: "/resume", command: "/resume", enabled: true, autoSend: true, desc: "\u4ECE\u6700\u8FD1\u7684\u4F1A\u8BDD\u7EE7\u7EED" },
|
|
180
|
+
{ label: "/exit", command: "/exit", enabled: true, autoSend: true, desc: "\u9000\u51FA Claude Code" }
|
|
181
|
+
]
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: "context",
|
|
185
|
+
title: "\u4E0A\u4E0B\u6587",
|
|
186
|
+
desc: "\u8BA9 Claude \u770B\u5230\u66F4\u591A\u4E0A\u4E0B\u6587\uFF1A\u52A0\u76EE\u5F55\u3001\u521D\u59CB\u5316\u9879\u76EE\u8BB0\u5FC6\u3001\u67E5\u770B\u8BB0\u5FC6\u3002",
|
|
187
|
+
items: [
|
|
188
|
+
{ label: "/add-dir", command: "/add-dir ", enabled: false, autoSend: false, desc: "\u628A\u989D\u5916\u76EE\u5F55\u52A0\u5165 Claude \u7684\u53EF\u8BFB\u8303\u56F4" },
|
|
189
|
+
{ label: "/init", command: "/init", enabled: false, autoSend: true, desc: "\u4E3A\u5F53\u524D\u9879\u76EE\u751F\u6210\u521D\u59CB CLAUDE.md" },
|
|
190
|
+
{ label: "/memory", command: "/memory", enabled: false, autoSend: true, desc: "\u67E5\u770B Claude \u5F53\u524D\u7684\u9879\u76EE\u8BB0\u5FC6" }
|
|
191
|
+
]
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
id: "agent",
|
|
195
|
+
title: "\u80FD\u529B",
|
|
196
|
+
desc: "Claude \u81EA\u8EAB\u7684\u80FD\u529B\u914D\u7F6E\uFF1A\u5B50 agent\u3001\u6A21\u578B\u3001hooks\u3001MCP\u3001\u8F93\u51FA\u98CE\u683C\u3002",
|
|
197
|
+
items: [
|
|
198
|
+
{ label: "/agents", command: "/agents", enabled: false, autoSend: true, desc: "\u7BA1\u7406\u53EF\u7528\u7684\u5B50 agent" },
|
|
199
|
+
{ label: "/model", command: "/model", enabled: false, autoSend: true, desc: "\u5207\u6362\u4F7F\u7528\u7684\u6A21\u578B" },
|
|
200
|
+
{ label: "/hooks", command: "/hooks", enabled: false, autoSend: true, desc: "\u7BA1\u7406\u751F\u547D\u5468\u671F hooks" },
|
|
201
|
+
{ label: "/mcp", command: "/mcp", enabled: false, autoSend: true, desc: "\u67E5\u770B / \u7BA1\u7406 MCP \u670D\u52A1\u5668" },
|
|
202
|
+
{ label: "/output-style", command: "/output-style", enabled: false, autoSend: true, desc: "\u5207\u6362\u8F93\u51FA\u98CE\u683C" },
|
|
203
|
+
{ label: "/output-style:new", command: "/output-style:new ", enabled: false, autoSend: false, desc: "\u65B0\u5EFA\u4E00\u4E2A\u8F93\u51FA\u98CE\u683C\uFF08\u9700\u8FFD\u52A0\u63CF\u8FF0\uFF09" }
|
|
204
|
+
]
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: "workflow",
|
|
208
|
+
title: "\u5DE5\u4F5C\u6D41",
|
|
209
|
+
desc: "\u4EFB\u52A1\u6E05\u5355\u3001\u8BBE\u7F6E\u3001\u6743\u9650\u3001\u72B6\u6001\u5C55\u793A\u3002",
|
|
210
|
+
items: [
|
|
211
|
+
{ label: "/todos", command: "/todos", enabled: false, autoSend: true, desc: "\u67E5\u770B\u5F53\u524D\u7684 TODO \u5217\u8868" },
|
|
212
|
+
{ label: "/config", command: "/config", enabled: false, autoSend: true, desc: "\u67E5\u770B / \u4FEE\u6539 Claude Code \u8BBE\u7F6E" },
|
|
213
|
+
{ label: "/permissions", command: "/permissions", enabled: false, autoSend: true, desc: "\u67E5\u770B / \u4FEE\u6539\u5DE5\u5177\u8C03\u7528\u6743\u9650" },
|
|
214
|
+
{ label: "/status", command: "/status", enabled: false, autoSend: true, desc: "\u67E5\u770B\u5F53\u524D\u4F1A\u8BDD\u72B6\u6001" },
|
|
215
|
+
{ label: "/statusline", command: "/statusline", enabled: false, autoSend: true, desc: "\u914D\u7F6E\u72B6\u6001\u884C\u663E\u793A" },
|
|
216
|
+
{ label: "/context", command: "/context", enabled: false, autoSend: true, desc: "\u67E5\u770B\u5F53\u524D\u4E0A\u4E0B\u6587\u4F7F\u7528\u60C5\u51B5" }
|
|
217
|
+
]
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
id: "auth",
|
|
221
|
+
title: "\u8D26\u53F7",
|
|
222
|
+
desc: "\u767B\u5F55\u3001\u767B\u51FA\u4E0E\u8D26\u53F7\u5207\u6362\u3002",
|
|
223
|
+
items: [
|
|
224
|
+
{ label: "/login", command: "/login", enabled: false, autoSend: true, desc: "\u767B\u5F55 / \u5207\u6362 Anthropic \u8D26\u53F7" },
|
|
225
|
+
{ label: "/logout", command: "/logout", enabled: false, autoSend: true, desc: "\u767B\u51FA\u5F53\u524D\u8D26\u53F7" }
|
|
226
|
+
]
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
id: "help",
|
|
230
|
+
title: "\u4FE1\u606F",
|
|
231
|
+
desc: "\u5E2E\u52A9\u3001\u8D39\u7528\u3001\u8BCA\u65AD\u3001PR \u8BC4\u8BBA\u3001\u7248\u672C\u8BF4\u660E\u3002",
|
|
232
|
+
items: [
|
|
233
|
+
{ label: "/help", command: "/help", enabled: false, autoSend: true, desc: "\u67E5\u770B\u53EF\u7528\u7684\u659C\u6760\u547D\u4EE4" },
|
|
234
|
+
{ label: "/cost", command: "/cost", enabled: false, autoSend: true, desc: "\u67E5\u770B\u672C\u4F1A\u8BDD\u7D2F\u8BA1\u6D88\u8017" },
|
|
235
|
+
{ label: "/doctor", command: "/doctor", enabled: false, autoSend: true, desc: "\u8FD0\u884C\u73AF\u5883\u5065\u5EB7\u68C0\u67E5" },
|
|
236
|
+
{ label: "/pr_comments", command: "/pr_comments", enabled: false, autoSend: true, desc: "\u6293\u53D6\u5E76\u9605\u8BFB\u5F53\u524D PR \u4E0A\u7684\u8BC4\u8BBA" },
|
|
237
|
+
{ label: "/release-notes", command: "/release-notes", enabled: false, autoSend: true, desc: "\u67E5\u770B\u6700\u65B0\u7248\u672C\u8BF4\u660E" }
|
|
238
|
+
]
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
id: "tools",
|
|
242
|
+
title: "\u5DE5\u5177",
|
|
243
|
+
desc: "\u6742\u9879\u5DE5\u5177\uFF1A\u540E\u53F0 bash\u3001GitHub App\u3001\u5B89\u5168\u5BA1\u67E5\u3001\u7EC8\u7AEF\u8BBE\u7F6E\u3001\u5347\u7EA7\u7B49\u3002",
|
|
244
|
+
items: [
|
|
245
|
+
{ label: "/bashes", command: "/bashes", enabled: false, autoSend: true, desc: "\u67E5\u770B\u540E\u53F0 bash \u8FDB\u7A0B" },
|
|
246
|
+
{ label: "/install-github-app", command: "/install-github-app", enabled: false, autoSend: true, desc: "\u5B89\u88C5 / \u914D\u7F6E GitHub App" },
|
|
247
|
+
{ label: "/migrate-installer", command: "/migrate-installer", enabled: false, autoSend: true, desc: "\u8FC1\u79FB\u5230\u539F\u751F\u5B89\u88C5\u5668" },
|
|
248
|
+
{ label: "/security-review", command: "/security-review", enabled: false, autoSend: true, desc: "\u8BA9 Claude \u505A\u4E00\u6B21\u5B89\u5168\u5BA1\u67E5" },
|
|
249
|
+
{ label: "/terminal-setup", command: "/terminal-setup", enabled: false, autoSend: true, desc: "\u914D\u7F6E\u7EC8\u7AEF\u96C6\u6210" },
|
|
250
|
+
{ label: "/upgrade", command: "/upgrade", enabled: false, autoSend: true, desc: "\u5347\u7EA7\u5230\u6700\u65B0 Claude Code" },
|
|
251
|
+
{ label: "/vim", command: "/vim", enabled: false, autoSend: true, desc: "\u5207\u6362 Vim \u952E\u4F4D" }
|
|
252
|
+
]
|
|
253
|
+
}
|
|
254
|
+
];
|
|
255
|
+
DEFAULT_COMMANDS = COMMAND_GROUPS.flatMap((g) => g.items.map((c) => ({ ...c, group: g.id })));
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// shared/dist/errors.js
|
|
260
|
+
var ErrorCode;
|
|
261
|
+
var init_errors = __esm({
|
|
262
|
+
"shared/dist/errors.js"() {
|
|
263
|
+
"use strict";
|
|
264
|
+
(function(ErrorCode2) {
|
|
265
|
+
ErrorCode2["AUTH_INVALID_TOKEN"] = "AUTH_INVALID_TOKEN";
|
|
266
|
+
ErrorCode2["AUTH_SESSION_EXPIRED"] = "AUTH_SESSION_EXPIRED";
|
|
267
|
+
ErrorCode2["AUTH_RATE_LIMITED"] = "AUTH_RATE_LIMITED";
|
|
268
|
+
ErrorCode2["AUTH_UNAUTHORIZED"] = "AUTH_UNAUTHORIZED";
|
|
269
|
+
ErrorCode2["AUTH_TOKEN_MISSING"] = "AUTH_TOKEN_MISSING";
|
|
270
|
+
ErrorCode2["PTY_SPAWN_FAILED"] = "PTY_SPAWN_FAILED";
|
|
271
|
+
ErrorCode2["PTY_NOT_RUNNING"] = "PTY_NOT_RUNNING";
|
|
272
|
+
ErrorCode2["PTY_RESIZE_FAILED"] = "PTY_RESIZE_FAILED";
|
|
273
|
+
ErrorCode2["PTY_WRITE_FAILED"] = "PTY_WRITE_FAILED";
|
|
274
|
+
ErrorCode2["WS_INVALID_MESSAGE"] = "WS_INVALID_MESSAGE";
|
|
275
|
+
ErrorCode2["WS_PAYLOAD_TOO_LARGE"] = "WS_PAYLOAD_TOO_LARGE";
|
|
276
|
+
ErrorCode2["WS_CONNECTION_CLOSED"] = "WS_CONNECTION_CLOSED";
|
|
277
|
+
ErrorCode2["CONFIG_PARSE_ERROR"] = "CONFIG_PARSE_ERROR";
|
|
278
|
+
ErrorCode2["CONFIG_VALIDATION_FAIL"] = "CONFIG_VALIDATION_FAIL";
|
|
279
|
+
ErrorCode2["CONFIG_WRITE_FAILED"] = "CONFIG_WRITE_FAILED";
|
|
280
|
+
ErrorCode2["INSTANCE_NOT_FOUND"] = "INSTANCE_NOT_FOUND";
|
|
281
|
+
ErrorCode2["INSTANCE_ALREADY_RUNNING"] = "INSTANCE_ALREADY_RUNNING";
|
|
282
|
+
ErrorCode2["PORT_UNAVAILABLE"] = "PORT_UNAVAILABLE";
|
|
283
|
+
ErrorCode2["WORKSPACE_FORBIDDEN"] = "WORKSPACE_FORBIDDEN";
|
|
284
|
+
ErrorCode2["CWD_NOT_EXIST"] = "CWD_NOT_EXIST";
|
|
285
|
+
ErrorCode2["LOCK_TIMEOUT"] = "LOCK_TIMEOUT";
|
|
286
|
+
ErrorCode2["LOCK_RELEASE_FAILED"] = "LOCK_RELEASE_FAILED";
|
|
287
|
+
ErrorCode2["PUSH_VAPID_NOT_READY"] = "PUSH_VAPID_NOT_READY";
|
|
288
|
+
ErrorCode2["PUSH_SUBSCRIPTION_INVALID"] = "PUSH_SUBSCRIPTION_INVALID";
|
|
289
|
+
ErrorCode2["PUSH_SEND_FAILED"] = "PUSH_SEND_FAILED";
|
|
290
|
+
ErrorCode2["HOOK_INVALID_PAYLOAD"] = "HOOK_INVALID_PAYLOAD";
|
|
291
|
+
ErrorCode2["HOOK_NON_LOCALHOST"] = "HOOK_NON_LOCALHOST";
|
|
292
|
+
ErrorCode2["INTERNAL_ERROR"] = "INTERNAL_ERROR";
|
|
293
|
+
ErrorCode2["NOT_IMPLEMENTED"] = "NOT_IMPLEMENTED";
|
|
294
|
+
})(ErrorCode || (ErrorCode = {}));
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// shared/dist/index.js
|
|
299
|
+
var init_dist = __esm({
|
|
300
|
+
"shared/dist/index.js"() {
|
|
301
|
+
"use strict";
|
|
302
|
+
init_constants();
|
|
303
|
+
init_ws_protocol();
|
|
304
|
+
init_instance();
|
|
305
|
+
init_defaults();
|
|
306
|
+
init_errors();
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// backend/dist/errors.js
|
|
311
|
+
var AppError, AuthError, PtyError, ConfigError, InstanceError, LockError, HookError, PushError;
|
|
312
|
+
var init_errors2 = __esm({
|
|
313
|
+
"backend/dist/errors.js"() {
|
|
314
|
+
"use strict";
|
|
315
|
+
init_dist();
|
|
316
|
+
AppError = class extends Error {
|
|
317
|
+
code;
|
|
318
|
+
httpStatus;
|
|
319
|
+
cause;
|
|
320
|
+
constructor(code, message, httpStatus = 500, cause) {
|
|
321
|
+
super(message);
|
|
322
|
+
this.name = this.constructor.name;
|
|
323
|
+
this.code = code;
|
|
324
|
+
this.httpStatus = httpStatus;
|
|
325
|
+
this.cause = cause;
|
|
326
|
+
if (cause instanceof Error && cause.stack) {
|
|
327
|
+
this.stack = `${this.stack ?? ""}
|
|
328
|
+
Caused by: ${cause.stack}`;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* 转换为协议层 ErrorPayload,用于 HTTP 响应或 WS error 消息
|
|
333
|
+
*/
|
|
334
|
+
toPayload() {
|
|
335
|
+
return {
|
|
336
|
+
code: this.code,
|
|
337
|
+
message: this.message
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
AuthError = class extends AppError {
|
|
342
|
+
constructor(code, message, httpStatus = 401, cause) {
|
|
343
|
+
super(code, message, httpStatus, cause);
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
PtyError = class extends AppError {
|
|
347
|
+
constructor(code, message, httpStatus = 500, cause) {
|
|
348
|
+
super(code, message, httpStatus, cause);
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
ConfigError = class extends AppError {
|
|
352
|
+
constructor(code, message, httpStatus = 500, cause) {
|
|
353
|
+
super(code, message, httpStatus, cause);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
InstanceError = class extends AppError {
|
|
357
|
+
constructor(code, message, httpStatus = 400, cause) {
|
|
358
|
+
super(code, message, httpStatus, cause);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
LockError = class extends AppError {
|
|
362
|
+
constructor(code, message, httpStatus = 503, cause) {
|
|
363
|
+
super(code, message, httpStatus, cause);
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
HookError = class extends AppError {
|
|
367
|
+
constructor(code, message, httpStatus = 400, cause) {
|
|
368
|
+
super(code, message, httpStatus, cause);
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
PushError = class extends AppError {
|
|
372
|
+
constructor(code, message, httpStatus = 500, cause) {
|
|
373
|
+
super(code, message, httpStatus, cause);
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// backend/dist/cli-utils.js
|
|
380
|
+
var cli_utils_exports = {};
|
|
381
|
+
__export(cli_utils_exports, {
|
|
382
|
+
HELP_TEXT: () => HELP_TEXT,
|
|
383
|
+
parseCliArgs: () => parseCliArgs
|
|
384
|
+
});
|
|
385
|
+
function parseCliArgs(argv) {
|
|
386
|
+
const result = {
|
|
387
|
+
subcommand: "start",
|
|
388
|
+
claudeArgs: []
|
|
389
|
+
};
|
|
390
|
+
let cursor = 0;
|
|
391
|
+
if (argv[0] && !argv[0].startsWith("-")) {
|
|
392
|
+
const sub = argv[0];
|
|
393
|
+
if (sub === "attach" || sub === "stop" || sub === "list") {
|
|
394
|
+
result.subcommand = sub;
|
|
395
|
+
cursor = 1;
|
|
396
|
+
if (sub === "attach") {
|
|
397
|
+
if (!argv[1] || argv[1].startsWith("-")) {
|
|
398
|
+
throw new ConfigError(ErrorCode.CONFIG_VALIDATION_FAIL, "attach \u5B50\u547D\u4EE4\u9700\u8981 URL \u53C2\u6570\uFF1Aatr attach <url>");
|
|
399
|
+
}
|
|
400
|
+
result.attachUrl = argv[1];
|
|
401
|
+
cursor = 2;
|
|
402
|
+
} else if (sub === "stop") {
|
|
403
|
+
if (argv[1] && !argv[1].startsWith("-")) {
|
|
404
|
+
result.stopPattern = argv[1];
|
|
405
|
+
cursor = 2;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
} else {
|
|
409
|
+
result.command = sub;
|
|
410
|
+
cursor = 1;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const programGiven = () => result.command !== void 0;
|
|
414
|
+
for (; cursor < argv.length; cursor++) {
|
|
415
|
+
let arg = argv[cursor];
|
|
416
|
+
if (arg === "--") {
|
|
417
|
+
result.claudeArgs.push(...argv.slice(cursor + 1));
|
|
418
|
+
return result;
|
|
419
|
+
}
|
|
420
|
+
if (arg.length === 2 && arg.startsWith("-") && !arg.startsWith("--")) {
|
|
421
|
+
const mapped = SHORT_TO_LONG[arg];
|
|
422
|
+
if (mapped !== void 0) {
|
|
423
|
+
arg = mapped;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (arg.startsWith("--") && arg.includes("=")) {
|
|
427
|
+
const eq = arg.indexOf("=");
|
|
428
|
+
const key = arg.slice(0, eq);
|
|
429
|
+
const val = arg.slice(eq + 1);
|
|
430
|
+
if (KNOWN_FLAGS_BOOL.has(key) || KNOWN_FLAGS_VALUE.has(key)) {
|
|
431
|
+
assignFlag(result, key, val);
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (programGiven()) {
|
|
435
|
+
result.claudeArgs.push(arg);
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
throw new ConfigError(ErrorCode.CONFIG_VALIDATION_FAIL, `\u672A\u77E5\u53C2\u6570\uFF1A${arg}`);
|
|
439
|
+
}
|
|
440
|
+
if (KNOWN_FLAGS_BOOL.has(arg)) {
|
|
441
|
+
assignFlag(result, arg, true);
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
if (KNOWN_FLAGS_VALUE.has(arg)) {
|
|
445
|
+
const val = argv[cursor + 1];
|
|
446
|
+
if (val === void 0) {
|
|
447
|
+
throw new ConfigError(ErrorCode.CONFIG_VALIDATION_FAIL, `\u53C2\u6570 ${arg} \u7F3A\u5C11\u503C`);
|
|
448
|
+
}
|
|
449
|
+
assignFlag(result, arg, val);
|
|
450
|
+
cursor++;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (programGiven()) {
|
|
454
|
+
result.claudeArgs.push(arg);
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
throw new ConfigError(ErrorCode.CONFIG_VALIDATION_FAIL, `\u672A\u77E5\u53C2\u6570\uFF1A${arg}`);
|
|
458
|
+
}
|
|
459
|
+
return result;
|
|
460
|
+
}
|
|
461
|
+
function assignFlag(out, key, value) {
|
|
462
|
+
switch (key) {
|
|
463
|
+
case "--no-terminal":
|
|
464
|
+
out.noTerminal = value === true || value === "true";
|
|
465
|
+
return;
|
|
466
|
+
case "--no-color":
|
|
467
|
+
out.noColor = value === true || value === "true";
|
|
468
|
+
return;
|
|
469
|
+
case "--no-open":
|
|
470
|
+
out.noOpen = value === true || value === "true";
|
|
471
|
+
return;
|
|
472
|
+
case "--wait-confirm":
|
|
473
|
+
out.waitConfirm = value === true || value === "true";
|
|
474
|
+
return;
|
|
475
|
+
case "--strict-port":
|
|
476
|
+
out.strictPort = value === true || value === "true";
|
|
477
|
+
return;
|
|
478
|
+
case "--help":
|
|
479
|
+
out.help = true;
|
|
480
|
+
return;
|
|
481
|
+
case "--version":
|
|
482
|
+
out.version = true;
|
|
483
|
+
return;
|
|
484
|
+
case "--port":
|
|
485
|
+
out.port = parsePort(value);
|
|
486
|
+
return;
|
|
487
|
+
case "--host":
|
|
488
|
+
out.host = String(value);
|
|
489
|
+
return;
|
|
490
|
+
case "--token":
|
|
491
|
+
out.token = String(value);
|
|
492
|
+
return;
|
|
493
|
+
case "--workdir":
|
|
494
|
+
case "--cwd":
|
|
495
|
+
out.workdir = String(value);
|
|
496
|
+
return;
|
|
497
|
+
case "--config":
|
|
498
|
+
out.configPath = String(value);
|
|
499
|
+
return;
|
|
500
|
+
case "--instance-name":
|
|
501
|
+
out.instanceName = String(value);
|
|
502
|
+
return;
|
|
503
|
+
case "--max-buffer-lines":
|
|
504
|
+
out.maxBufferLines = parsePositiveInt(key, value);
|
|
505
|
+
return;
|
|
506
|
+
case "--session-ttl":
|
|
507
|
+
out.sessionTtlMs = parsePositiveInt(key, value);
|
|
508
|
+
return;
|
|
509
|
+
case "--auth-rate-limit":
|
|
510
|
+
out.authRateLimit = parsePositiveInt(key, value);
|
|
511
|
+
return;
|
|
512
|
+
case "--spawn-timeout":
|
|
513
|
+
out.spawnTimeoutSec = parseNonNegativeInt(key, value);
|
|
514
|
+
return;
|
|
515
|
+
case "--dev-proxy":
|
|
516
|
+
out.devProxy = parsePort(value);
|
|
517
|
+
return;
|
|
518
|
+
case "--log-dir":
|
|
519
|
+
out.logDir = String(value);
|
|
520
|
+
return;
|
|
521
|
+
default:
|
|
522
|
+
throw new ConfigError(ErrorCode.CONFIG_VALIDATION_FAIL, `\u672A\u77E5\u53C2\u6570\uFF1A${key}`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function parsePort(value) {
|
|
526
|
+
if (typeof value !== "string") {
|
|
527
|
+
throw new ConfigError(ErrorCode.CONFIG_VALIDATION_FAIL, "--port \u9700\u8981\u6570\u503C");
|
|
528
|
+
}
|
|
529
|
+
const n = Number(value);
|
|
530
|
+
if (!Number.isInteger(n) || n <= 0 || n > 65535) {
|
|
531
|
+
throw new ConfigError(ErrorCode.CONFIG_VALIDATION_FAIL, `--port \u975E\u6CD5\uFF1A${value}`);
|
|
532
|
+
}
|
|
533
|
+
return n;
|
|
534
|
+
}
|
|
535
|
+
function parsePositiveInt(name, value) {
|
|
536
|
+
if (typeof value !== "string") {
|
|
537
|
+
throw new ConfigError(ErrorCode.CONFIG_VALIDATION_FAIL, `${name} \u9700\u8981\u6570\u503C`);
|
|
538
|
+
}
|
|
539
|
+
const n = Number(value);
|
|
540
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
541
|
+
throw new ConfigError(ErrorCode.CONFIG_VALIDATION_FAIL, `${name} \u975E\u6CD5\uFF1A${value}`);
|
|
542
|
+
}
|
|
543
|
+
return n;
|
|
544
|
+
}
|
|
545
|
+
function parseNonNegativeInt(name, value) {
|
|
546
|
+
if (typeof value !== "string") {
|
|
547
|
+
throw new ConfigError(ErrorCode.CONFIG_VALIDATION_FAIL, `${name} \u9700\u8981\u6570\u503C`);
|
|
548
|
+
}
|
|
549
|
+
const n = Number(value);
|
|
550
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
551
|
+
throw new ConfigError(ErrorCode.CONFIG_VALIDATION_FAIL, `${name} \u975E\u6CD5\uFF1A${value}`);
|
|
552
|
+
}
|
|
553
|
+
return n;
|
|
554
|
+
}
|
|
555
|
+
var KNOWN_FLAGS_BOOL, KNOWN_FLAGS_VALUE, SHORT_TO_LONG, HELP_TEXT;
|
|
556
|
+
var init_cli_utils = __esm({
|
|
557
|
+
"backend/dist/cli-utils.js"() {
|
|
558
|
+
"use strict";
|
|
559
|
+
init_errors2();
|
|
560
|
+
init_dist();
|
|
561
|
+
KNOWN_FLAGS_BOOL = /* @__PURE__ */ new Set([
|
|
562
|
+
"--no-terminal",
|
|
563
|
+
"--no-color",
|
|
564
|
+
"--help",
|
|
565
|
+
"--version",
|
|
566
|
+
"--no-open",
|
|
567
|
+
"--wait-confirm",
|
|
568
|
+
"--strict-port"
|
|
569
|
+
]);
|
|
570
|
+
KNOWN_FLAGS_VALUE = /* @__PURE__ */ new Set([
|
|
571
|
+
"--port",
|
|
572
|
+
"--host",
|
|
573
|
+
"--token",
|
|
574
|
+
"--workdir",
|
|
575
|
+
"--cwd",
|
|
576
|
+
"--config",
|
|
577
|
+
"--instance-name",
|
|
578
|
+
"--max-buffer-lines",
|
|
579
|
+
"--session-ttl",
|
|
580
|
+
"--auth-rate-limit",
|
|
581
|
+
"--log-dir",
|
|
582
|
+
"--spawn-timeout",
|
|
583
|
+
"--dev-proxy"
|
|
584
|
+
]);
|
|
585
|
+
SHORT_TO_LONG = {
|
|
586
|
+
"-p": "--port",
|
|
587
|
+
"-h": "--help",
|
|
588
|
+
"-v": "--version",
|
|
589
|
+
"-S": "--strict-port"
|
|
590
|
+
};
|
|
591
|
+
HELP_TEXT = `atr \u2014 auvezy/terminal-remote \xB7 \u5C40\u57DF\u7F51\u5185\u8FDC\u7A0B\u8BBF\u95EE PC \u7EC8\u7AEF\u7684\u4EE3\u7406
|
|
592
|
+
|
|
593
|
+
\u7528\u6CD5\uFF1A
|
|
594
|
+
atr \u542F\u52A8\uFF0CPTY \u8DD1\u5F53\u524D $SHELL
|
|
595
|
+
atr <program> [args...] \u542F\u52A8\uFF0CPTY \u8DD1 program\uFF08args \u900F\u4F20\uFF09
|
|
596
|
+
\u4F8B\uFF1Aatr zsh / atr claude / atr claude --resume
|
|
597
|
+
atr attach <url> \u63A5\u7BA1\u5DF2\u6709\u5B9E\u4F8B
|
|
598
|
+
atr list \u5217\u51FA\u672C\u673A\u6240\u6709\u5B9E\u4F8B
|
|
599
|
+
atr stop [pattern] \u505C\u6B62\u5339\u914D\u7684\u5B9E\u4F8B
|
|
600
|
+
|
|
601
|
+
\u542F\u52A8\u9009\u9879\uFF1A
|
|
602
|
+
-p, --port <n> \u76D1\u542C\u7AEF\u53E3\uFF08\u9ED8\u8BA4 3000\uFF0C\u88AB\u5360\u7528\u81EA\u52A8 +1\uFF0C\u9664\u975E\u542F\u7528 -S\uFF09
|
|
603
|
+
--host <ip> \u76D1\u542C host\uFF08\u9ED8\u8BA4\u81EA\u52A8\u68C0\u6D4B LAN IP\uFF09
|
|
604
|
+
-S, --strict-port \u4E25\u683C\u7AEF\u53E3\u6A21\u5F0F\uFF1Apreferred \u7AEF\u53E3\u88AB\u5360\u5373\u62A5\u9519\uFF0C\u4E0D\u81EA\u9002\u5E94
|
|
605
|
+
--spawn-timeout <s> PTY \u515C\u5E95\u8D85\u65F6\u79D2\u6570\uFF08\u9ED8\u8BA4 30\uFF1B0 = \u65E0\u8D85\u65F6\uFF09\u3002
|
|
606
|
+
\u4E0E --wait-confirm \u4E92\u65A5\uFF08\u540E\u8005\u5F3A\u5236 Enter\uFF0C\u5FFD\u7565\u672C\u9879\u4E0E\u6D4F\u89C8\u5668\u89E6\u53D1\uFF09
|
|
607
|
+
--token <hex> \u6307\u5B9A Token\uFF08\u9ED8\u8BA4\u4ECE\u5171\u4EAB\u6587\u4EF6\u8BFB\u6216\u751F\u6210\uFF09
|
|
608
|
+
--workdir <path> \u5B50\u8FDB\u7A0B\u5DE5\u4F5C\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09
|
|
609
|
+
--instance-name <s> \u5B9E\u4F8B\u663E\u793A\u540D\uFF08\u9ED8\u8BA4\u5DE5\u4F5C\u76EE\u5F55\u6700\u540E\u4E00\u6BB5\uFF09
|
|
610
|
+
--config <path> config.json \u8DEF\u5F84\uFF08\u9ED8\u8BA4 ~/.auvezy/terminal-remote/config.json\uFF09
|
|
611
|
+
--max-buffer-lines \u8F93\u51FA\u7F13\u51B2\u884C\u6570\uFF08\u9ED8\u8BA4 10000\uFF09
|
|
612
|
+
--session-ttl <ms> Session \u6709\u6548\u671F\uFF0C\u6BEB\u79D2\uFF08\u9ED8\u8BA4 24h\uFF09
|
|
613
|
+
--auth-rate-limit <n> \u6BCF\u5206\u949F\u6BCF IP \u8BA4\u8BC1\u6B21\u6570\u4E0A\u9650\uFF08\u9ED8\u8BA4 20\uFF09
|
|
614
|
+
--log-dir <path> \u65E5\u5FD7\u76EE\u5F55\u8986\u76D6
|
|
615
|
+
--no-terminal \u4E0D\u5728\u672C\u8FDB\u7A0B stdout \u663E\u793A PTY \u8F93\u51FA
|
|
616
|
+
--no-color \u7981\u7528\u5F69\u8272\u8F93\u51FA
|
|
617
|
+
--no-open \u4E0D\u81EA\u52A8\u6253\u5F00\u6D4F\u89C8\u5668
|
|
618
|
+
--wait-confirm \u542F\u52A8 backend \u540E\u7B49\u7528\u6237\u6309 Enter \u624D spawn \u5B50\u8FDB\u7A0B
|
|
619
|
+
\uFF08\u9ED8\u8BA4\u7ACB\u5373 spawn\uFF1B\u9002\u5408\u4E0D\u5E0C\u671B\u5168\u5C4F TUI \u7ACB\u523B\u8986\u76D6 banner \u7684\u573A\u666F\uFF09
|
|
620
|
+
-h, --help \u663E\u793A\u672C\u5E2E\u52A9
|
|
621
|
+
-v, --version \u663E\u793A\u7248\u672C\u53F7
|
|
622
|
+
|
|
623
|
+
-- <args> \u4E4B\u540E\u7684\u53C2\u6570\u4E5F\u4F1A\u900F\u4F20\u7ED9 program\uFF08\u4E0E program \u540E\u4F4D\u7F6E\u53C2\u6570\u7B49\u4EF7\uFF09
|
|
624
|
+
|
|
625
|
+
\u591A\u5B9E\u4F8B\uFF1A
|
|
626
|
+
\u5728\u4E0D\u540C\u7EC8\u7AEF\u591A\u6B21\u6267\u884C atr\uFF0C\u4F1A\u81EA\u52A8\u5360\u7528 3000\u30013001\u30013002\u2026\uFF0C
|
|
627
|
+
\u6BCF\u4E2A\u5B9E\u4F8B\u72EC\u7ACB PTY\uFF1B\u6D4F\u89C8\u5668\u9876\u680F\u7684\u5B9E\u4F8B tab \u53EF\u4E00\u952E\u5207\u6362\u3002
|
|
628
|
+
`;
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
// backend/dist/logger/logger.js
|
|
633
|
+
import pino from "pino";
|
|
634
|
+
import { resolve, dirname } from "node:path";
|
|
635
|
+
import { fileURLToPath } from "node:url";
|
|
636
|
+
import { mkdirSync } from "node:fs";
|
|
637
|
+
function createLogger() {
|
|
638
|
+
if (isTest) {
|
|
639
|
+
return pino({ level: "silent" });
|
|
640
|
+
}
|
|
641
|
+
const transport = pino.transport({
|
|
642
|
+
targets: [
|
|
643
|
+
{
|
|
644
|
+
target: "pino/file",
|
|
645
|
+
level: isCli ? "warn" : "info",
|
|
646
|
+
options: {
|
|
647
|
+
destination: 2,
|
|
648
|
+
// fd 2 = stderr
|
|
649
|
+
colorize: isStderrTty
|
|
650
|
+
}
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
target: "pino/file",
|
|
654
|
+
level: "info",
|
|
655
|
+
options: { destination: appLogPath, mkdir: true }
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
target: "pino/file",
|
|
659
|
+
level: "error",
|
|
660
|
+
options: { destination: errorLogPath, mkdir: true }
|
|
661
|
+
}
|
|
662
|
+
]
|
|
663
|
+
});
|
|
664
|
+
return pino({
|
|
665
|
+
level: "debug",
|
|
666
|
+
mixin() {
|
|
667
|
+
return currentInstancePort !== null ? { instancePort: currentInstancePort } : {};
|
|
668
|
+
}
|
|
669
|
+
}, transport);
|
|
670
|
+
}
|
|
671
|
+
var __dirname, projectRoot, logDir, isTest, isCli, isStderrTty, appLogPath, errorLogPath, currentInstancePort, logger;
|
|
672
|
+
var init_logger = __esm({
|
|
673
|
+
"backend/dist/logger/logger.js"() {
|
|
674
|
+
"use strict";
|
|
675
|
+
__dirname = dirname(fileURLToPath(import.meta.url));
|
|
676
|
+
projectRoot = resolve(__dirname, "..", "..", "..");
|
|
677
|
+
logDir = process.env["LOG_DIR"] ?? resolve(projectRoot, "logs");
|
|
678
|
+
isTest = process.env["NODE_ENV"] === "test" || process.env["VITEST"] === "true";
|
|
679
|
+
isCli = process.env["CLI_MODE"] === "true";
|
|
680
|
+
isStderrTty = process.stderr.isTTY === true;
|
|
681
|
+
try {
|
|
682
|
+
mkdirSync(logDir, { recursive: true });
|
|
683
|
+
} catch {
|
|
684
|
+
}
|
|
685
|
+
appLogPath = resolve(logDir, "app.log");
|
|
686
|
+
errorLogPath = resolve(logDir, "error.log");
|
|
687
|
+
currentInstancePort = null;
|
|
688
|
+
logger = createLogger();
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
// backend/dist/api/health-routes.js
|
|
693
|
+
import { Router } from "express";
|
|
694
|
+
function createHealthRoutes() {
|
|
695
|
+
const router = Router();
|
|
696
|
+
router.get("/health", (_req, res) => {
|
|
697
|
+
res.json({
|
|
698
|
+
ok: true,
|
|
699
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
700
|
+
uptime: Math.floor(process.uptime())
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
return router;
|
|
704
|
+
}
|
|
705
|
+
var init_health_routes = __esm({
|
|
706
|
+
"backend/dist/api/health-routes.js"() {
|
|
707
|
+
"use strict";
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
// backend/dist/api/auth-routes.js
|
|
712
|
+
import { Router as Router2 } from "express";
|
|
713
|
+
function createAuthRoutes(authModule) {
|
|
714
|
+
const router = Router2();
|
|
715
|
+
router.post("/auth", authModule.handleAuth);
|
|
716
|
+
return router;
|
|
717
|
+
}
|
|
718
|
+
var init_auth_routes = __esm({
|
|
719
|
+
"backend/dist/api/auth-routes.js"() {
|
|
720
|
+
"use strict";
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
// backend/dist/api/hook-routes.js
|
|
725
|
+
import { Router as Router3 } from "express";
|
|
726
|
+
function isLoopback(ip) {
|
|
727
|
+
return ip === "127.0.0.1" || ip === "::1" || ip === "::ffff:127.0.0.1";
|
|
728
|
+
}
|
|
729
|
+
function createHookRoutes(receiver) {
|
|
730
|
+
const router = Router3();
|
|
731
|
+
router.post("/hook", (req, res) => {
|
|
732
|
+
const ip = req.ip ?? req.socket.remoteAddress ?? "";
|
|
733
|
+
if (!isLoopback(ip)) {
|
|
734
|
+
logger.warn({ ip }, "/api/hook \u62D2\u7EDD\uFF1A\u975E loopback \u6765\u6E90");
|
|
735
|
+
const err = new HookError(ErrorCode.HOOK_NON_LOCALHOST, "/api/hook \u4EC5\u63A5\u53D7 localhost \u8C03\u7528", 403);
|
|
736
|
+
res.status(err.httpStatus).json({ error: err.toPayload() });
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
const payload = req.body;
|
|
740
|
+
if (!payload || typeof payload !== "object") {
|
|
741
|
+
const err = new HookError(ErrorCode.HOOK_INVALID_PAYLOAD, "hook payload \u5FC5\u987B\u662F JSON \u5BF9\u8C61", 400);
|
|
742
|
+
res.status(err.httpStatus).json({ error: err.toPayload() });
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
const result = receiver.processHook(payload);
|
|
746
|
+
if (result.type === "notification") {
|
|
747
|
+
res.json({ ok: true, tool: result.notification.tool });
|
|
748
|
+
} else {
|
|
749
|
+
res.json({ ok: true, ignored: true, reason: result.reason });
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
return router;
|
|
753
|
+
}
|
|
754
|
+
var init_hook_routes = __esm({
|
|
755
|
+
"backend/dist/api/hook-routes.js"() {
|
|
756
|
+
"use strict";
|
|
757
|
+
init_dist();
|
|
758
|
+
init_errors2();
|
|
759
|
+
init_logger();
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
// backend/dist/api/config-routes.js
|
|
764
|
+
import { Router as Router4 } from "express";
|
|
765
|
+
function createConfigRoutes(authModule, store) {
|
|
766
|
+
const router = Router4();
|
|
767
|
+
router.get("/config", authModule.requireAuth, (_req, res) => {
|
|
768
|
+
const value = ensureDefaultUserConfig(store.get());
|
|
769
|
+
res.json({ ok: true, config: value });
|
|
770
|
+
});
|
|
771
|
+
router.put("/config", authModule.requireAuth, (req, res) => {
|
|
772
|
+
const body = req.body;
|
|
773
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
774
|
+
const err = new ConfigError(ErrorCode.CONFIG_VALIDATION_FAIL, "PUT /api/config body \u5FC5\u987B\u662F JSON \u5BF9\u8C61", 400);
|
|
775
|
+
res.status(err.httpStatus).json({ error: err.toPayload() });
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
const incoming = body;
|
|
779
|
+
if (incoming.shortcuts !== void 0 && !Array.isArray(incoming.shortcuts)) {
|
|
780
|
+
return rejectFieldType(res, "shortcuts");
|
|
781
|
+
}
|
|
782
|
+
if (incoming.commands !== void 0 && !Array.isArray(incoming.commands)) {
|
|
783
|
+
return rejectFieldType(res, "commands");
|
|
784
|
+
}
|
|
785
|
+
if (incoming.fontScale !== void 0 && typeof incoming.fontScale !== "number") {
|
|
786
|
+
return rejectFieldType(res, "fontScale");
|
|
787
|
+
}
|
|
788
|
+
if (incoming.vapidPublicKey !== void 0 && typeof incoming.vapidPublicKey !== "string") {
|
|
789
|
+
return rejectFieldType(res, "vapidPublicKey");
|
|
790
|
+
}
|
|
791
|
+
try {
|
|
792
|
+
store.set(incoming);
|
|
793
|
+
} catch (err) {
|
|
794
|
+
logger.error({ err }, "PUT /api/config \u5199\u5165\u5931\u8D25");
|
|
795
|
+
if (err instanceof ConfigError) {
|
|
796
|
+
res.status(err.httpStatus).json({ error: err.toPayload() });
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
const fallback = new ConfigError(ErrorCode.CONFIG_WRITE_FAILED, "\u914D\u7F6E\u5199\u5165\u5931\u8D25", 500, err);
|
|
800
|
+
res.status(fallback.httpStatus).json({ error: fallback.toPayload() });
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
res.json({ ok: true, config: ensureDefaultUserConfig(store.get()) });
|
|
804
|
+
});
|
|
805
|
+
return router;
|
|
806
|
+
}
|
|
807
|
+
function rejectFieldType(res, field) {
|
|
808
|
+
const err = new ConfigError(ErrorCode.CONFIG_VALIDATION_FAIL, `\u5B57\u6BB5 ${field} \u7C7B\u578B\u4E0D\u6B63\u786E`, 400);
|
|
809
|
+
res.status(err.httpStatus).json({ error: err.toPayload() });
|
|
810
|
+
}
|
|
811
|
+
var init_config_routes = __esm({
|
|
812
|
+
"backend/dist/api/config-routes.js"() {
|
|
813
|
+
"use strict";
|
|
814
|
+
init_dist();
|
|
815
|
+
init_errors2();
|
|
816
|
+
init_logger();
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
// backend/dist/constants.js
|
|
821
|
+
var WS_FLUSH_INTERVAL_MS, WS_MAX_CHUNK_BYTES, WS_HIGH_WATERMARK_BYTES, FILE_LOCK_RETRIES, FILE_LOCK_RETRY_INTERVAL_MS, FILE_LOCK_STALE_MS, IP_MONITOR_INTERVAL_MS, IP_MONITOR_STABILITY_THRESHOLD, PTY_DEFAULT_COLS, PTY_DEFAULT_ROWS, PTY_TERM_NAME, SHUTDOWN_WS_FLUSH_DELAY_MS, SHUTDOWN_FORCE_EXIT_MS, DOUBLE_CTRL_C_WINDOW_MS, PORT_FINDER_MAX_ATTEMPTS, STOP_INSTANCE_GRACE_MS, STOP_INSTANCE_POLL_INTERVAL_MS, ATTACH_RECONNECT_DELAYS_MS;
|
|
822
|
+
var init_constants2 = __esm({
|
|
823
|
+
"backend/dist/constants.js"() {
|
|
824
|
+
"use strict";
|
|
825
|
+
WS_FLUSH_INTERVAL_MS = 16;
|
|
826
|
+
WS_MAX_CHUNK_BYTES = 32 * 1024;
|
|
827
|
+
WS_HIGH_WATERMARK_BYTES = 256 * 1024;
|
|
828
|
+
FILE_LOCK_RETRIES = 50;
|
|
829
|
+
FILE_LOCK_RETRY_INTERVAL_MS = 50;
|
|
830
|
+
FILE_LOCK_STALE_MS = 1e4;
|
|
831
|
+
IP_MONITOR_INTERVAL_MS = 3e4;
|
|
832
|
+
IP_MONITOR_STABILITY_THRESHOLD = 2;
|
|
833
|
+
PTY_DEFAULT_COLS = 80;
|
|
834
|
+
PTY_DEFAULT_ROWS = 24;
|
|
835
|
+
PTY_TERM_NAME = "xterm-256color";
|
|
836
|
+
SHUTDOWN_WS_FLUSH_DELAY_MS = 500;
|
|
837
|
+
SHUTDOWN_FORCE_EXIT_MS = 2e3;
|
|
838
|
+
DOUBLE_CTRL_C_WINDOW_MS = 500;
|
|
839
|
+
PORT_FINDER_MAX_ATTEMPTS = 100;
|
|
840
|
+
STOP_INSTANCE_GRACE_MS = 3e3;
|
|
841
|
+
STOP_INSTANCE_POLL_INTERVAL_MS = 100;
|
|
842
|
+
ATTACH_RECONNECT_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 16e3, 3e4];
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
// backend/dist/utils/file-lock.js
|
|
847
|
+
import { mkdirSync as mkdirSync2, writeFileSync, readFileSync, rmSync, statSync, existsSync } from "node:fs";
|
|
848
|
+
import { resolve as resolve2 } from "node:path";
|
|
849
|
+
async function withFileLock(lockDir, fn, opts = {}) {
|
|
850
|
+
const retries = opts.retries ?? FILE_LOCK_RETRIES;
|
|
851
|
+
const interval = opts.retryIntervalMs ?? FILE_LOCK_RETRY_INTERVAL_MS;
|
|
852
|
+
const staleMs = opts.staleMs ?? FILE_LOCK_STALE_MS;
|
|
853
|
+
let attempt = 0;
|
|
854
|
+
while (true) {
|
|
855
|
+
if (tryAcquireLock(lockDir, staleMs)) {
|
|
856
|
+
try {
|
|
857
|
+
return await fn();
|
|
858
|
+
} finally {
|
|
859
|
+
releaseLock(lockDir);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
attempt++;
|
|
863
|
+
if (attempt > retries) {
|
|
864
|
+
throw new LockError(ErrorCode.LOCK_TIMEOUT, `\u9501\u8D85\u65F6\uFF1A${lockDir}\uFF08\u91CD\u8BD5 ${retries} \u6B21\u4ECD\u672A\u83B7\u53D6\uFF09`);
|
|
865
|
+
}
|
|
866
|
+
await sleep(interval);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
function tryAcquireLock(lockDir, staleMs = FILE_LOCK_STALE_MS) {
|
|
870
|
+
if (existsSync(lockDir)) {
|
|
871
|
+
if (isStale(lockDir, staleMs)) {
|
|
872
|
+
logger.info({ lockDir }, "\u68C0\u6D4B\u5230\u50F5\u5C38\u9501\uFF0C\u81EA\u52A8\u6E05\u7406");
|
|
873
|
+
try {
|
|
874
|
+
rmSync(lockDir, { recursive: true, force: true });
|
|
875
|
+
} catch (err) {
|
|
876
|
+
logger.warn({ lockDir, err }, "\u6E05\u7406\u50F5\u5C38\u9501\u5931\u8D25\uFF08\u7EE7\u7EED\u5C1D\u8BD5\uFF09");
|
|
877
|
+
}
|
|
878
|
+
} else {
|
|
879
|
+
return false;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
try {
|
|
883
|
+
mkdirSync2(lockDir, { recursive: false, mode: 448 });
|
|
884
|
+
const pidFile = resolve2(lockDir, "pid.txt");
|
|
885
|
+
try {
|
|
886
|
+
writeFileSync(pidFile, `${process.pid}
|
|
887
|
+
${Date.now()}
|
|
888
|
+
`, {
|
|
889
|
+
encoding: "utf-8",
|
|
890
|
+
mode: 384
|
|
891
|
+
});
|
|
892
|
+
} catch (err) {
|
|
893
|
+
logger.warn({ pidFile, err }, "\u5199 pid.txt \u5931\u8D25\uFF08\u9501\u4ECD\u6301\u6709\uFF09");
|
|
894
|
+
}
|
|
895
|
+
return true;
|
|
896
|
+
} catch (err) {
|
|
897
|
+
if (err.code === "EEXIST")
|
|
898
|
+
return false;
|
|
899
|
+
logger.warn({ lockDir, err }, "mkdir \u5931\u8D25");
|
|
900
|
+
return false;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
function releaseLock(lockDir) {
|
|
904
|
+
try {
|
|
905
|
+
rmSync(lockDir, { recursive: true, force: true });
|
|
906
|
+
} catch (err) {
|
|
907
|
+
logger.warn({ lockDir, err }, "\u91CA\u653E\u9501\u5931\u8D25");
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
function isStale(lockDir, staleMs) {
|
|
911
|
+
const pidFile = resolve2(lockDir, "pid.txt");
|
|
912
|
+
if (existsSync(pidFile)) {
|
|
913
|
+
try {
|
|
914
|
+
const txt = readFileSync(pidFile, "utf-8");
|
|
915
|
+
const pid = parseInt(txt.split("\n")[0] ?? "", 10);
|
|
916
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
917
|
+
return true;
|
|
918
|
+
}
|
|
919
|
+
if (pid === process.pid) {
|
|
920
|
+
return false;
|
|
921
|
+
}
|
|
922
|
+
try {
|
|
923
|
+
process.kill(pid, 0);
|
|
924
|
+
return false;
|
|
925
|
+
} catch (err) {
|
|
926
|
+
return err.code === "ESRCH";
|
|
927
|
+
}
|
|
928
|
+
} catch {
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
try {
|
|
932
|
+
const st = statSync(lockDir);
|
|
933
|
+
return Date.now() - st.mtimeMs > staleMs;
|
|
934
|
+
} catch {
|
|
935
|
+
return false;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
function sleep(ms) {
|
|
939
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
940
|
+
}
|
|
941
|
+
var init_file_lock = __esm({
|
|
942
|
+
"backend/dist/utils/file-lock.js"() {
|
|
943
|
+
"use strict";
|
|
944
|
+
init_dist();
|
|
945
|
+
init_errors2();
|
|
946
|
+
init_logger();
|
|
947
|
+
init_constants2();
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
// backend/dist/utils/atomic-write.js
|
|
952
|
+
import { writeFileSync as writeFileSync2, renameSync } from "node:fs";
|
|
953
|
+
function atomicWriteJson(path, value, mode = 384) {
|
|
954
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
955
|
+
writeFileSync2(tmp, JSON.stringify(value, null, 2), { encoding: "utf-8", mode });
|
|
956
|
+
renameSync(tmp, path);
|
|
957
|
+
}
|
|
958
|
+
var init_atomic_write = __esm({
|
|
959
|
+
"backend/dist/utils/atomic-write.js"() {
|
|
960
|
+
"use strict";
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
// backend/dist/registry/instance-registry.js
|
|
965
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, mkdirSync as mkdirSync3 } from "node:fs";
|
|
966
|
+
import { resolve as resolve3 } from "node:path";
|
|
967
|
+
import { homedir } from "node:os";
|
|
968
|
+
function isPidAlive(pid) {
|
|
969
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
970
|
+
return false;
|
|
971
|
+
if (pid === process.pid)
|
|
972
|
+
return true;
|
|
973
|
+
try {
|
|
974
|
+
process.kill(pid, 0);
|
|
975
|
+
return true;
|
|
976
|
+
} catch (err) {
|
|
977
|
+
return err.code !== "ESRCH";
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
var InstanceRegistryManager;
|
|
981
|
+
var init_instance_registry = __esm({
|
|
982
|
+
"backend/dist/registry/instance-registry.js"() {
|
|
983
|
+
"use strict";
|
|
984
|
+
init_dist();
|
|
985
|
+
init_file_lock();
|
|
986
|
+
init_atomic_write();
|
|
987
|
+
init_logger();
|
|
988
|
+
InstanceRegistryManager = class {
|
|
989
|
+
baseDir;
|
|
990
|
+
path;
|
|
991
|
+
lockDir;
|
|
992
|
+
constructor(opts = {}) {
|
|
993
|
+
this.baseDir = opts.baseDir ?? resolve3(homedir(), ATR_DATA_DIR);
|
|
994
|
+
this.path = resolve3(this.baseDir, opts.filename ?? REGISTRY_FILENAME);
|
|
995
|
+
this.lockDir = `${this.path}.lock`;
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* 列出所有"活着"的实例(自动剔除 pid 已死的)
|
|
999
|
+
*/
|
|
1000
|
+
async list() {
|
|
1001
|
+
return withFileLock(this.lockDir, () => {
|
|
1002
|
+
const reg = this.readUnlocked();
|
|
1003
|
+
const alive = reg.instances.filter((i) => isPidAlive(i.pid));
|
|
1004
|
+
if (alive.length !== reg.instances.length) {
|
|
1005
|
+
this.writeUnlocked({ version: 1, instances: alive });
|
|
1006
|
+
}
|
|
1007
|
+
return alive;
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* 注册新实例(如果 instanceId 已存在则替换 = upsert)
|
|
1012
|
+
*/
|
|
1013
|
+
async register(info) {
|
|
1014
|
+
return withFileLock(this.lockDir, () => {
|
|
1015
|
+
const reg = this.readUnlocked();
|
|
1016
|
+
const filtered = reg.instances.filter((i) => i.instanceId !== info.instanceId && isPidAlive(i.pid));
|
|
1017
|
+
filtered.push(info);
|
|
1018
|
+
this.writeUnlocked({ version: 1, instances: filtered });
|
|
1019
|
+
logger.info({ instanceId: info.instanceId, port: info.port }, "\u5B9E\u4F8B\u5DF2\u6CE8\u518C");
|
|
1020
|
+
return filtered;
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* 注销实例(按 instanceId 删除)
|
|
1025
|
+
*
|
|
1026
|
+
* 进程退出时调用;找不到也不报错。
|
|
1027
|
+
*/
|
|
1028
|
+
async unregister(instanceId) {
|
|
1029
|
+
return withFileLock(this.lockDir, () => {
|
|
1030
|
+
const reg = this.readUnlocked();
|
|
1031
|
+
const next = reg.instances.filter((i) => i.instanceId !== instanceId && isPidAlive(i.pid));
|
|
1032
|
+
this.writeUnlocked({ version: 1, instances: next });
|
|
1033
|
+
logger.info({ instanceId }, "\u5B9E\u4F8B\u5DF2\u6CE8\u9500");
|
|
1034
|
+
return next;
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
/** 路径暴露给调用方做日志/调试用 */
|
|
1038
|
+
get filePath() {
|
|
1039
|
+
return this.path;
|
|
1040
|
+
}
|
|
1041
|
+
// ───────── 私有:未加锁的读写(仅在 withFileLock 回调内调用) ─────────
|
|
1042
|
+
readUnlocked() {
|
|
1043
|
+
if (!existsSync2(this.path))
|
|
1044
|
+
return { version: 1, instances: [] };
|
|
1045
|
+
try {
|
|
1046
|
+
const raw = readFileSync2(this.path, "utf-8");
|
|
1047
|
+
const parsed = JSON.parse(raw);
|
|
1048
|
+
if (parsed.version !== 1 || !Array.isArray(parsed.instances)) {
|
|
1049
|
+
logger.warn({ path: this.path }, "\u6CE8\u518C\u8868 schema \u4E0D\u8BC6\u522B\uFF0C\u91CD\u7F6E\u4E3A\u7A7A");
|
|
1050
|
+
return { version: 1, instances: [] };
|
|
1051
|
+
}
|
|
1052
|
+
return parsed;
|
|
1053
|
+
} catch (err) {
|
|
1054
|
+
logger.warn({ path: this.path, err }, "\u6CE8\u518C\u8868\u8BFB\u5931\u8D25\uFF0C\u91CD\u7F6E\u4E3A\u7A7A");
|
|
1055
|
+
return { version: 1, instances: [] };
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
writeUnlocked(reg) {
|
|
1059
|
+
if (!existsSync2(this.baseDir)) {
|
|
1060
|
+
mkdirSync3(this.baseDir, { recursive: true, mode: 448 });
|
|
1061
|
+
}
|
|
1062
|
+
atomicWriteJson(this.path, reg);
|
|
1063
|
+
}
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
// backend/dist/registry/stop-instances.js
|
|
1069
|
+
async function stopInstances(pattern, opts = {}) {
|
|
1070
|
+
const registry = opts.registry ?? new InstanceRegistryManager();
|
|
1071
|
+
const graceMs = opts.graceMs ?? STOP_INSTANCE_GRACE_MS;
|
|
1072
|
+
const pollMs = opts.pollIntervalMs ?? STOP_INSTANCE_POLL_INTERVAL_MS;
|
|
1073
|
+
const kill = opts.killImpl ?? defaultKill;
|
|
1074
|
+
const all = await registry.list();
|
|
1075
|
+
const targets = filterByPattern(all, pattern);
|
|
1076
|
+
const results = [];
|
|
1077
|
+
for (const inst of targets) {
|
|
1078
|
+
const r = await stopOne(inst, kill, graceMs, pollMs);
|
|
1079
|
+
results.push(r);
|
|
1080
|
+
try {
|
|
1081
|
+
await registry.unregister(inst.instanceId);
|
|
1082
|
+
} catch (err) {
|
|
1083
|
+
logger.warn({ err, instanceId: inst.instanceId }, "\u6CE8\u9500\u5931\u8D25\uFF08\u5FFD\u7565\uFF09");
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
return results;
|
|
1087
|
+
}
|
|
1088
|
+
function filterByPattern(all, pattern) {
|
|
1089
|
+
if (!pattern || pattern.length === 0)
|
|
1090
|
+
return all;
|
|
1091
|
+
const p = pattern.toLowerCase();
|
|
1092
|
+
return all.filter((i) => {
|
|
1093
|
+
const fields = [i.name, i.cwd, `${i.host}:${i.port}`].map((s) => s.toLowerCase());
|
|
1094
|
+
return fields.some((s) => s.includes(p));
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
async function stopOne(inst, kill, graceMs, pollMs) {
|
|
1098
|
+
if (!isPidAlive(inst.pid)) {
|
|
1099
|
+
return { instance: inst, outcome: "gone" };
|
|
1100
|
+
}
|
|
1101
|
+
try {
|
|
1102
|
+
kill(inst.pid, "SIGTERM");
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
return {
|
|
1105
|
+
instance: inst,
|
|
1106
|
+
outcome: "failed",
|
|
1107
|
+
error: err.message
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
const deadline = Date.now() + graceMs;
|
|
1111
|
+
while (Date.now() < deadline) {
|
|
1112
|
+
if (!isPidAlive(inst.pid)) {
|
|
1113
|
+
return { instance: inst, outcome: "sigterm" };
|
|
1114
|
+
}
|
|
1115
|
+
await sleep2(pollMs);
|
|
1116
|
+
}
|
|
1117
|
+
try {
|
|
1118
|
+
kill(inst.pid, "SIGKILL");
|
|
1119
|
+
} catch {
|
|
1120
|
+
return { instance: inst, outcome: "sigterm" };
|
|
1121
|
+
}
|
|
1122
|
+
await sleep2(pollMs * 2);
|
|
1123
|
+
return { instance: inst, outcome: "sigkill" };
|
|
1124
|
+
}
|
|
1125
|
+
function defaultKill(pid, signal) {
|
|
1126
|
+
process.kill(pid, signal);
|
|
1127
|
+
}
|
|
1128
|
+
function sleep2(ms) {
|
|
1129
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1130
|
+
}
|
|
1131
|
+
var init_stop_instances = __esm({
|
|
1132
|
+
"backend/dist/registry/stop-instances.js"() {
|
|
1133
|
+
"use strict";
|
|
1134
|
+
init_instance_registry();
|
|
1135
|
+
init_logger();
|
|
1136
|
+
init_constants2();
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
// backend/dist/registry/instance-events.js
|
|
1141
|
+
import { watch } from "node:fs";
|
|
1142
|
+
import { dirname as dirname2, basename } from "node:path";
|
|
1143
|
+
import { EventEmitter } from "node:events";
|
|
1144
|
+
function startInstanceWatcher(filePath) {
|
|
1145
|
+
if (watcher)
|
|
1146
|
+
return;
|
|
1147
|
+
const dir = dirname2(filePath);
|
|
1148
|
+
const fname = basename(filePath);
|
|
1149
|
+
try {
|
|
1150
|
+
watcher = watch(dir, (eventType, changedName) => {
|
|
1151
|
+
if (changedName !== fname)
|
|
1152
|
+
return;
|
|
1153
|
+
if (debounceTimer)
|
|
1154
|
+
clearTimeout(debounceTimer);
|
|
1155
|
+
debounceTimer = setTimeout(() => {
|
|
1156
|
+
debounceTimer = null;
|
|
1157
|
+
bus.emit("change");
|
|
1158
|
+
}, DEBOUNCE_MS);
|
|
1159
|
+
});
|
|
1160
|
+
watcher.on("error", (err) => {
|
|
1161
|
+
logger.warn({ err }, "instances.json watcher \u5F02\u5E38\uFF08\u5DF2\u505C\u6B62\uFF09");
|
|
1162
|
+
watcher = null;
|
|
1163
|
+
});
|
|
1164
|
+
logger.info({ dir, fname }, "instances.json watcher \u5DF2\u542F\u52A8");
|
|
1165
|
+
} catch (err) {
|
|
1166
|
+
logger.warn({ err, dir }, "instances.json watch \u542F\u52A8\u5931\u8D25\uFF08SSE \u5C06\u65E0 push\uFF0C\u4EC5\u9760\u524D\u7AEF\u8F6E\u8BE2\uFF09");
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
function stopInstanceWatcher() {
|
|
1170
|
+
if (debounceTimer) {
|
|
1171
|
+
clearTimeout(debounceTimer);
|
|
1172
|
+
debounceTimer = null;
|
|
1173
|
+
}
|
|
1174
|
+
if (watcher) {
|
|
1175
|
+
watcher.close();
|
|
1176
|
+
watcher = null;
|
|
1177
|
+
}
|
|
1178
|
+
bus.removeAllListeners();
|
|
1179
|
+
}
|
|
1180
|
+
function onInstanceChange(cb) {
|
|
1181
|
+
bus.on("change", cb);
|
|
1182
|
+
return () => bus.off("change", cb);
|
|
1183
|
+
}
|
|
1184
|
+
var DEBOUNCE_MS, InstanceEventBus, bus, watcher, debounceTimer;
|
|
1185
|
+
var init_instance_events = __esm({
|
|
1186
|
+
"backend/dist/registry/instance-events.js"() {
|
|
1187
|
+
"use strict";
|
|
1188
|
+
init_logger();
|
|
1189
|
+
DEBOUNCE_MS = 100;
|
|
1190
|
+
InstanceEventBus = class extends EventEmitter {
|
|
1191
|
+
};
|
|
1192
|
+
bus = new InstanceEventBus();
|
|
1193
|
+
bus.setMaxListeners(0);
|
|
1194
|
+
watcher = null;
|
|
1195
|
+
debounceTimer = null;
|
|
1196
|
+
}
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
// backend/dist/api/instance-routes.js
|
|
1200
|
+
import { Router as Router5 } from "express";
|
|
1201
|
+
function createInstanceRoutes(opts) {
|
|
1202
|
+
const router = Router5();
|
|
1203
|
+
const { authModule, registry, currentInstanceId } = opts;
|
|
1204
|
+
router.get("/instances", authModule.requireAuth, async (req, res) => {
|
|
1205
|
+
logger.debug({ ip: req.ip }, "GET /instances");
|
|
1206
|
+
try {
|
|
1207
|
+
const list = await registry.list();
|
|
1208
|
+
const items = list.map((i) => ({
|
|
1209
|
+
...i,
|
|
1210
|
+
isCurrent: i.instanceId === currentInstanceId
|
|
1211
|
+
}));
|
|
1212
|
+
res.json({ ok: true, instances: items });
|
|
1213
|
+
} catch (err) {
|
|
1214
|
+
logger.error({ err }, "GET /instances \u5931\u8D25");
|
|
1215
|
+
const e = err instanceof InstanceError ? err : new InstanceError(ErrorCode.INTERNAL_ERROR, "\u6CE8\u518C\u8868\u8BFB\u53D6\u5931\u8D25", 500, err);
|
|
1216
|
+
res.status(e.httpStatus).json({ error: e.toPayload() });
|
|
1217
|
+
}
|
|
1218
|
+
});
|
|
1219
|
+
router.get("/instances/stream", authModule.requireAuth, async (req, res) => {
|
|
1220
|
+
res.set({
|
|
1221
|
+
"Content-Type": "text/event-stream",
|
|
1222
|
+
"Cache-Control": "no-cache, no-transform",
|
|
1223
|
+
Connection: "keep-alive",
|
|
1224
|
+
// 防 nginx 缓冲(即使我们没 nginx,加上无害)
|
|
1225
|
+
"X-Accel-Buffering": "no"
|
|
1226
|
+
});
|
|
1227
|
+
res.flushHeaders();
|
|
1228
|
+
const sendSnapshot = async () => {
|
|
1229
|
+
try {
|
|
1230
|
+
const list = await registry.list();
|
|
1231
|
+
const items = list.map((i) => ({
|
|
1232
|
+
...i,
|
|
1233
|
+
isCurrent: i.instanceId === currentInstanceId
|
|
1234
|
+
}));
|
|
1235
|
+
res.write(`event: instances
|
|
1236
|
+
data: ${JSON.stringify({ instances: items })}
|
|
1237
|
+
|
|
1238
|
+
`);
|
|
1239
|
+
} catch (err) {
|
|
1240
|
+
logger.warn({ err }, "SSE snapshot \u63A8\u9001\u5931\u8D25\uFF08\u975E\u81F4\u547D\uFF09");
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
await sendSnapshot();
|
|
1244
|
+
const unsubscribe = onInstanceChange(() => {
|
|
1245
|
+
void sendSnapshot();
|
|
1246
|
+
});
|
|
1247
|
+
const heartbeat = setInterval(() => {
|
|
1248
|
+
try {
|
|
1249
|
+
res.write(`:keepalive
|
|
1250
|
+
|
|
1251
|
+
`);
|
|
1252
|
+
} catch {
|
|
1253
|
+
}
|
|
1254
|
+
}, 25e3);
|
|
1255
|
+
const cleanup = () => {
|
|
1256
|
+
clearInterval(heartbeat);
|
|
1257
|
+
unsubscribe();
|
|
1258
|
+
};
|
|
1259
|
+
req.on("close", cleanup);
|
|
1260
|
+
req.on("aborted", cleanup);
|
|
1261
|
+
});
|
|
1262
|
+
router.post("/instances", authModule.requireAuth, async (req, res) => {
|
|
1263
|
+
if (!opts.spawner) {
|
|
1264
|
+
const e = new InstanceError(ErrorCode.INTERNAL_ERROR, "\u5F53\u524D\u90E8\u7F72\u4E0D\u652F\u6301 Web \u521B\u5EFA\u5B9E\u4F8B", 501);
|
|
1265
|
+
res.status(e.httpStatus).json({ error: e.toPayload() });
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
const body = req.body;
|
|
1269
|
+
if (!body || typeof body !== "object" || typeof body.cwd !== "string") {
|
|
1270
|
+
const e = new InstanceError(ErrorCode.CWD_NOT_EXIST, "POST /instances \u9700\u8981 body.cwd\uFF08\u5B57\u7B26\u4E32\uFF09", 400);
|
|
1271
|
+
res.status(e.httpStatus).json({ error: e.toPayload() });
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
try {
|
|
1275
|
+
const result = await opts.spawner.spawn({
|
|
1276
|
+
cwd: body.cwd,
|
|
1277
|
+
name: typeof body.name === "string" ? body.name : void 0
|
|
1278
|
+
});
|
|
1279
|
+
res.json({ ok: true, instance: result });
|
|
1280
|
+
} catch (err) {
|
|
1281
|
+
logger.error({ err }, "POST /instances \u5931\u8D25");
|
|
1282
|
+
const e = err instanceof InstanceError ? err : new InstanceError(ErrorCode.INTERNAL_ERROR, "\u6D3E\u751F\u5B9E\u4F8B\u5931\u8D25", 500, err);
|
|
1283
|
+
res.status(e.httpStatus).json({ error: e.toPayload() });
|
|
1284
|
+
}
|
|
1285
|
+
});
|
|
1286
|
+
router.delete("/instances/:id", authModule.requireAuth, async (req, res) => {
|
|
1287
|
+
const id = req.params.id;
|
|
1288
|
+
logger.debug({ id, ip: req.ip, currentInstanceId }, "DELETE /instances/:id");
|
|
1289
|
+
if (!id || typeof id !== "string") {
|
|
1290
|
+
const e = new InstanceError(ErrorCode.CWD_NOT_EXIST, "instanceId \u5FC5\u586B", 400);
|
|
1291
|
+
res.status(e.httpStatus).json({ error: e.toPayload() });
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
if (id === currentInstanceId) {
|
|
1295
|
+
const e = new InstanceError(ErrorCode.INTERNAL_ERROR, "\u4E0D\u80FD\u901A\u8FC7 API \u505C\u6B62\u5F53\u524D\u5B9E\u4F8B\uFF08\u4F1A\u8BA9\u8FDE\u63A5\u4F60\u81EA\u5DF1\u7684\u8FDB\u7A0B\u9000\u51FA\uFF09", 400);
|
|
1296
|
+
res.status(e.httpStatus).json({ error: e.toPayload() });
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
try {
|
|
1300
|
+
const all = await registry.list();
|
|
1301
|
+
const target = all.find((i) => i.instanceId === id);
|
|
1302
|
+
if (!target) {
|
|
1303
|
+
const e = new InstanceError(ErrorCode.INTERNAL_ERROR, "\u5B9E\u4F8B\u4E0D\u5B58\u5728", 404);
|
|
1304
|
+
res.status(e.httpStatus).json({ error: e.toPayload() });
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
const pattern = `${target.host}:${target.port}`;
|
|
1308
|
+
const results = await stopInstances(pattern, { registry });
|
|
1309
|
+
const r = results.find((x) => x.instance.instanceId === id);
|
|
1310
|
+
if (!r) {
|
|
1311
|
+
const e = new InstanceError(ErrorCode.INTERNAL_ERROR, `stopInstances \u672A\u547D\u4E2D ${pattern}`, 500);
|
|
1312
|
+
res.status(e.httpStatus).json({ error: e.toPayload() });
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
res.json({ ok: true, outcome: r.outcome });
|
|
1316
|
+
} catch (err) {
|
|
1317
|
+
logger.error({ err, id }, "DELETE /instances/:id \u5931\u8D25");
|
|
1318
|
+
const e = err instanceof InstanceError ? err : new InstanceError(ErrorCode.INTERNAL_ERROR, "\u505C\u6B62\u5B9E\u4F8B\u5931\u8D25", 500, err);
|
|
1319
|
+
res.status(e.httpStatus).json({ error: e.toPayload() });
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
return router;
|
|
1323
|
+
}
|
|
1324
|
+
var init_instance_routes = __esm({
|
|
1325
|
+
"backend/dist/api/instance-routes.js"() {
|
|
1326
|
+
"use strict";
|
|
1327
|
+
init_dist();
|
|
1328
|
+
init_stop_instances();
|
|
1329
|
+
init_instance_events();
|
|
1330
|
+
init_errors2();
|
|
1331
|
+
init_logger();
|
|
1332
|
+
}
|
|
1333
|
+
});
|
|
1334
|
+
|
|
1335
|
+
// backend/dist/api/push-routes.js
|
|
1336
|
+
import { Router as Router6 } from "express";
|
|
1337
|
+
function createPushRoutes(authModule, push) {
|
|
1338
|
+
const router = Router6();
|
|
1339
|
+
router.get("/push/vapid", (_req, res) => {
|
|
1340
|
+
try {
|
|
1341
|
+
res.json({ ok: true, publicKey: push.getPublicKey() });
|
|
1342
|
+
} catch (err) {
|
|
1343
|
+
const e = err instanceof PushError ? err : new PushError(ErrorCode.INTERNAL_ERROR, "\u83B7\u53D6 VAPID \u5931\u8D25", 500, err);
|
|
1344
|
+
res.status(e.httpStatus).json({ error: e.toPayload() });
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
router.post("/push/subscriptions", authModule.requireAuth, (req, res) => {
|
|
1348
|
+
const body = req.body;
|
|
1349
|
+
if (!body || typeof body !== "object") {
|
|
1350
|
+
const e = new PushError(ErrorCode.PUSH_SUBSCRIPTION_INVALID, "\u8BA2\u9605 body \u5FC5\u987B\u662F JSON \u5BF9\u8C61", 400);
|
|
1351
|
+
res.status(e.httpStatus).json({ error: e.toPayload() });
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
try {
|
|
1355
|
+
push.subscribe(body);
|
|
1356
|
+
res.json({ ok: true });
|
|
1357
|
+
} catch (err) {
|
|
1358
|
+
const e = err instanceof PushError ? err : new PushError(ErrorCode.INTERNAL_ERROR, "\u8BA2\u9605\u5931\u8D25", 500, err);
|
|
1359
|
+
logger.warn({ err }, "\u8BA2\u9605\u5931\u8D25");
|
|
1360
|
+
res.status(e.httpStatus).json({ error: e.toPayload() });
|
|
1361
|
+
}
|
|
1362
|
+
});
|
|
1363
|
+
router.delete("/push/subscriptions", authModule.requireAuth, (req, res) => {
|
|
1364
|
+
const body = req.body;
|
|
1365
|
+
const endpoint = typeof body?.endpoint === "string" ? body.endpoint : "";
|
|
1366
|
+
if (!endpoint) {
|
|
1367
|
+
const e = new PushError(ErrorCode.PUSH_SUBSCRIPTION_INVALID, "\u53D6\u6D88\u8BA2\u9605\u9700\u8981 body.endpoint", 400);
|
|
1368
|
+
res.status(e.httpStatus).json({ error: e.toPayload() });
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
const removed = push.unsubscribe(endpoint);
|
|
1372
|
+
res.json({ ok: true, removed });
|
|
1373
|
+
});
|
|
1374
|
+
return router;
|
|
1375
|
+
}
|
|
1376
|
+
var init_push_routes = __esm({
|
|
1377
|
+
"backend/dist/api/push-routes.js"() {
|
|
1378
|
+
"use strict";
|
|
1379
|
+
init_dist();
|
|
1380
|
+
init_errors2();
|
|
1381
|
+
init_logger();
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
// backend/dist/utils/network.js
|
|
1386
|
+
import { networkInterfaces } from "node:os";
|
|
1387
|
+
function isPrivateIp(ip) {
|
|
1388
|
+
if (ip.includes(":"))
|
|
1389
|
+
return false;
|
|
1390
|
+
const parts = ip.split(".");
|
|
1391
|
+
if (parts.length !== 4)
|
|
1392
|
+
return false;
|
|
1393
|
+
const [a, b] = parts.map((s) => Number(s));
|
|
1394
|
+
if (!Number.isInteger(a) || !Number.isInteger(b) || a === void 0 || b === void 0) {
|
|
1395
|
+
return false;
|
|
1396
|
+
}
|
|
1397
|
+
if (a === 10)
|
|
1398
|
+
return true;
|
|
1399
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
1400
|
+
return true;
|
|
1401
|
+
if (a === 192 && b === 168)
|
|
1402
|
+
return true;
|
|
1403
|
+
return false;
|
|
1404
|
+
}
|
|
1405
|
+
function isLinkLocal(ip) {
|
|
1406
|
+
if (ip.includes(":"))
|
|
1407
|
+
return false;
|
|
1408
|
+
const parts = ip.split(".");
|
|
1409
|
+
if (parts.length !== 4)
|
|
1410
|
+
return false;
|
|
1411
|
+
const [a, b] = parts.map((s) => Number(s));
|
|
1412
|
+
return a === 169 && b === 254;
|
|
1413
|
+
}
|
|
1414
|
+
function isTailscaleIp(ip) {
|
|
1415
|
+
if (ip.includes(":"))
|
|
1416
|
+
return false;
|
|
1417
|
+
const parts = ip.split(".");
|
|
1418
|
+
if (parts.length !== 4)
|
|
1419
|
+
return false;
|
|
1420
|
+
const [a, b] = parts.map((s) => Number(s));
|
|
1421
|
+
if (!Number.isInteger(a) || !Number.isInteger(b) || a === void 0 || b === void 0) {
|
|
1422
|
+
return false;
|
|
1423
|
+
}
|
|
1424
|
+
return a === 100 && b >= 64 && b <= 127;
|
|
1425
|
+
}
|
|
1426
|
+
function isLoopbackIp(ip) {
|
|
1427
|
+
if (ip.includes(":")) {
|
|
1428
|
+
return ip === "::1";
|
|
1429
|
+
}
|
|
1430
|
+
const parts = ip.split(".");
|
|
1431
|
+
if (parts.length !== 4)
|
|
1432
|
+
return false;
|
|
1433
|
+
return Number(parts[0]) === 127;
|
|
1434
|
+
}
|
|
1435
|
+
function detectDisplayIp(hostHint) {
|
|
1436
|
+
if (hostHint && hostHint !== "0.0.0.0" && hostHint !== "::" && !isLoopbackIp(hostHint)) {
|
|
1437
|
+
return hostHint;
|
|
1438
|
+
}
|
|
1439
|
+
const ifaces = networkInterfaces();
|
|
1440
|
+
const privates = [];
|
|
1441
|
+
const linkLocals = [];
|
|
1442
|
+
for (const list of Object.values(ifaces)) {
|
|
1443
|
+
if (!list)
|
|
1444
|
+
continue;
|
|
1445
|
+
for (const info of list) {
|
|
1446
|
+
if (info.internal)
|
|
1447
|
+
continue;
|
|
1448
|
+
if (info.family !== "IPv4")
|
|
1449
|
+
continue;
|
|
1450
|
+
const ip = info.address;
|
|
1451
|
+
if (isPrivateIp(ip)) {
|
|
1452
|
+
privates.push(ip);
|
|
1453
|
+
} else if (isLinkLocal(ip)) {
|
|
1454
|
+
linkLocals.push(ip);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
return privates[0] ?? linkLocals[0] ?? "127.0.0.1";
|
|
1459
|
+
}
|
|
1460
|
+
function isShareableIpv6(ip, info) {
|
|
1461
|
+
if (!ip.includes(":"))
|
|
1462
|
+
return false;
|
|
1463
|
+
const lower = ip.toLowerCase();
|
|
1464
|
+
if (lower.startsWith("fe8") || lower.startsWith("fe9") || lower.startsWith("fea") || lower.startsWith("feb"))
|
|
1465
|
+
return false;
|
|
1466
|
+
if (lower === "::1" || lower === "::" || lower.startsWith("ff"))
|
|
1467
|
+
return false;
|
|
1468
|
+
if (info?.scopeid !== void 0 && info.scopeid !== 0)
|
|
1469
|
+
return false;
|
|
1470
|
+
return true;
|
|
1471
|
+
}
|
|
1472
|
+
function buildPublicUrl(displayIp, port, token) {
|
|
1473
|
+
const base = `http://${displayIp}:${port}/`;
|
|
1474
|
+
if (!token)
|
|
1475
|
+
return base;
|
|
1476
|
+
return `${base}?token=${encodeURIComponent(token)}`;
|
|
1477
|
+
}
|
|
1478
|
+
var init_network = __esm({
|
|
1479
|
+
"backend/dist/utils/network.js"() {
|
|
1480
|
+
"use strict";
|
|
1481
|
+
}
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
// backend/dist/api/share-routes.js
|
|
1485
|
+
import { Router as Router7 } from "express";
|
|
1486
|
+
import { networkInterfaces as networkInterfaces2 } from "node:os";
|
|
1487
|
+
function createShareRoutes(opts) {
|
|
1488
|
+
const router = Router7();
|
|
1489
|
+
const { authModule, port, displayIp } = opts;
|
|
1490
|
+
router.get("/share/endpoints", authModule.requireAuth, (_req, res) => {
|
|
1491
|
+
const endpoints = collectEndpoints(port, displayIp);
|
|
1492
|
+
res.json({ ok: true, endpoints });
|
|
1493
|
+
});
|
|
1494
|
+
return router;
|
|
1495
|
+
}
|
|
1496
|
+
function collectEndpoints(port, displayIp) {
|
|
1497
|
+
const out = [];
|
|
1498
|
+
const ifaces = networkInterfaces2();
|
|
1499
|
+
for (const [ifname, list] of Object.entries(ifaces)) {
|
|
1500
|
+
if (!list)
|
|
1501
|
+
continue;
|
|
1502
|
+
let ipv6KeptForIface = false;
|
|
1503
|
+
for (const info of list) {
|
|
1504
|
+
if (info.internal)
|
|
1505
|
+
continue;
|
|
1506
|
+
const ip = info.address;
|
|
1507
|
+
if (info.family === "IPv6") {
|
|
1508
|
+
if (!isShareableIpv6(ip, { scopeid: info.scopeid }))
|
|
1509
|
+
continue;
|
|
1510
|
+
if (ipv6KeptForIface)
|
|
1511
|
+
continue;
|
|
1512
|
+
ipv6KeptForIface = true;
|
|
1513
|
+
}
|
|
1514
|
+
const kind = classify(ip, info.family);
|
|
1515
|
+
out.push({ host: ip, port, kind, interface: ifname });
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
if (!out.some((e) => e.kind === "loopback")) {
|
|
1519
|
+
out.push({ host: "127.0.0.1", port, kind: "loopback" });
|
|
1520
|
+
}
|
|
1521
|
+
const order = {
|
|
1522
|
+
lan: 0,
|
|
1523
|
+
tailscale: 1,
|
|
1524
|
+
other: 2,
|
|
1525
|
+
ipv6: 3,
|
|
1526
|
+
loopback: 4
|
|
1527
|
+
};
|
|
1528
|
+
out.sort((a, b) => {
|
|
1529
|
+
if (a.kind !== b.kind)
|
|
1530
|
+
return order[a.kind] - order[b.kind];
|
|
1531
|
+
if (a.host === displayIp)
|
|
1532
|
+
return -1;
|
|
1533
|
+
if (b.host === displayIp)
|
|
1534
|
+
return 1;
|
|
1535
|
+
return 0;
|
|
1536
|
+
});
|
|
1537
|
+
let defaultIdx = out.findIndex((e) => e.host === displayIp);
|
|
1538
|
+
if (defaultIdx === -1) {
|
|
1539
|
+
defaultIdx = out.findIndex((e) => e.kind !== "loopback");
|
|
1540
|
+
}
|
|
1541
|
+
if (defaultIdx === -1 && out.length > 0)
|
|
1542
|
+
defaultIdx = 0;
|
|
1543
|
+
if (defaultIdx >= 0)
|
|
1544
|
+
out[defaultIdx].isDefault = true;
|
|
1545
|
+
return out;
|
|
1546
|
+
}
|
|
1547
|
+
function classify(ip, family) {
|
|
1548
|
+
if (isLoopbackIp(ip))
|
|
1549
|
+
return "loopback";
|
|
1550
|
+
if (family === "IPv6")
|
|
1551
|
+
return "ipv6";
|
|
1552
|
+
if (isTailscaleIp(ip))
|
|
1553
|
+
return "tailscale";
|
|
1554
|
+
if (isPrivateIp(ip))
|
|
1555
|
+
return "lan";
|
|
1556
|
+
if (isLinkLocal(ip))
|
|
1557
|
+
return "other";
|
|
1558
|
+
return "other";
|
|
1559
|
+
}
|
|
1560
|
+
var init_share_routes = __esm({
|
|
1561
|
+
"backend/dist/api/share-routes.js"() {
|
|
1562
|
+
"use strict";
|
|
1563
|
+
init_network();
|
|
1564
|
+
}
|
|
1565
|
+
});
|
|
1566
|
+
|
|
1567
|
+
// backend/dist/api/router.js
|
|
1568
|
+
import { Router as Router8 } from "express";
|
|
1569
|
+
function createApiRouter(opts = {}) {
|
|
1570
|
+
const router = Router8();
|
|
1571
|
+
router.use(createHealthRoutes());
|
|
1572
|
+
if (opts.authModule) {
|
|
1573
|
+
router.use(createAuthRoutes(opts.authModule));
|
|
1574
|
+
}
|
|
1575
|
+
if (opts.authModule && opts.configStore) {
|
|
1576
|
+
router.use(createConfigRoutes(opts.authModule, opts.configStore));
|
|
1577
|
+
}
|
|
1578
|
+
if (opts.hookReceiver) {
|
|
1579
|
+
router.use(createHookRoutes(opts.hookReceiver));
|
|
1580
|
+
}
|
|
1581
|
+
if (opts.authModule && opts.registry && opts.currentInstanceId) {
|
|
1582
|
+
router.use(createInstanceRoutes({
|
|
1583
|
+
authModule: opts.authModule,
|
|
1584
|
+
registry: opts.registry,
|
|
1585
|
+
currentInstanceId: opts.currentInstanceId,
|
|
1586
|
+
spawner: opts.spawner
|
|
1587
|
+
}));
|
|
1588
|
+
}
|
|
1589
|
+
if (opts.authModule && opts.pushService) {
|
|
1590
|
+
router.use(createPushRoutes(opts.authModule, opts.pushService));
|
|
1591
|
+
}
|
|
1592
|
+
if (opts.authModule && typeof opts.port === "number" && opts.displayIp) {
|
|
1593
|
+
router.use(createShareRoutes({
|
|
1594
|
+
authModule: opts.authModule,
|
|
1595
|
+
port: opts.port,
|
|
1596
|
+
displayIp: opts.displayIp
|
|
1597
|
+
}));
|
|
1598
|
+
}
|
|
1599
|
+
return router;
|
|
1600
|
+
}
|
|
1601
|
+
var init_router = __esm({
|
|
1602
|
+
"backend/dist/api/router.js"() {
|
|
1603
|
+
"use strict";
|
|
1604
|
+
init_health_routes();
|
|
1605
|
+
init_auth_routes();
|
|
1606
|
+
init_hook_routes();
|
|
1607
|
+
init_config_routes();
|
|
1608
|
+
init_instance_routes();
|
|
1609
|
+
init_push_routes();
|
|
1610
|
+
init_share_routes();
|
|
1611
|
+
}
|
|
1612
|
+
});
|
|
1613
|
+
|
|
1614
|
+
// backend/dist/pty/pty-manager.js
|
|
1615
|
+
import { EventEmitter as EventEmitter2 } from "node:events";
|
|
1616
|
+
import * as pty from "node-pty";
|
|
1617
|
+
var PtyManager;
|
|
1618
|
+
var init_pty_manager = __esm({
|
|
1619
|
+
"backend/dist/pty/pty-manager.js"() {
|
|
1620
|
+
"use strict";
|
|
1621
|
+
init_constants2();
|
|
1622
|
+
init_errors2();
|
|
1623
|
+
init_dist();
|
|
1624
|
+
init_logger();
|
|
1625
|
+
PtyManager = class extends EventEmitter2 {
|
|
1626
|
+
process = null;
|
|
1627
|
+
_exited = false;
|
|
1628
|
+
_cols = PTY_DEFAULT_COLS;
|
|
1629
|
+
_rows = PTY_DEFAULT_ROWS;
|
|
1630
|
+
get cols() {
|
|
1631
|
+
return this._cols;
|
|
1632
|
+
}
|
|
1633
|
+
get rows() {
|
|
1634
|
+
return this._rows;
|
|
1635
|
+
}
|
|
1636
|
+
/** 进程是否已退出(exit 事件后置 true) */
|
|
1637
|
+
get exited() {
|
|
1638
|
+
return this._exited;
|
|
1639
|
+
}
|
|
1640
|
+
/**
|
|
1641
|
+
* 启动 PTY 进程
|
|
1642
|
+
*
|
|
1643
|
+
* @throws {PtyError} 已经有进程在运行时(避免一个 manager spawn 两次)
|
|
1644
|
+
*
|
|
1645
|
+
* spawn 失败不抛错,而是 emit 'error' 事件——便于上层用一致方式处理
|
|
1646
|
+
*/
|
|
1647
|
+
spawn(opts) {
|
|
1648
|
+
if (this.process) {
|
|
1649
|
+
throw new PtyError(ErrorCode.INSTANCE_ALREADY_RUNNING, "PTY \u5DF2\u7ECF\u5728\u8FD0\u884C\uFF0C\u4E0D\u80FD\u91CD\u590D spawn");
|
|
1650
|
+
}
|
|
1651
|
+
const cols = opts.cols ?? process.stdout.columns ?? PTY_DEFAULT_COLS;
|
|
1652
|
+
const rows = opts.rows ?? process.stdout.rows ?? PTY_DEFAULT_ROWS;
|
|
1653
|
+
this._cols = cols;
|
|
1654
|
+
this._rows = rows;
|
|
1655
|
+
logger.info({ command: opts.command, args: opts.args, cwd: opts.cwd, cols, rows }, "\u6B63\u5728\u542F\u52A8 PTY \u8FDB\u7A0B");
|
|
1656
|
+
try {
|
|
1657
|
+
this.process = pty.spawn(opts.command, opts.args ?? [], {
|
|
1658
|
+
name: PTY_TERM_NAME,
|
|
1659
|
+
cols,
|
|
1660
|
+
rows,
|
|
1661
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
1662
|
+
env: { ...process.env, ...opts.env }
|
|
1663
|
+
});
|
|
1664
|
+
this.process.onData((data) => {
|
|
1665
|
+
this.emit("data", data);
|
|
1666
|
+
});
|
|
1667
|
+
this.process.onExit(({ exitCode, signal }) => {
|
|
1668
|
+
this._exited = true;
|
|
1669
|
+
logger.info({ exitCode, signal }, "PTY \u8FDB\u7A0B\u9000\u51FA");
|
|
1670
|
+
this.emit("exit", exitCode, signal);
|
|
1671
|
+
this.process = null;
|
|
1672
|
+
});
|
|
1673
|
+
logger.info({ pid: this.process.pid, cols, rows }, "PTY \u8FDB\u7A0B\u5DF2\u542F\u52A8");
|
|
1674
|
+
} catch (err) {
|
|
1675
|
+
logger.error({ err }, "spawn PTY \u8FDB\u7A0B\u5931\u8D25");
|
|
1676
|
+
const wrapped = new PtyError(ErrorCode.PTY_SPAWN_FAILED, err instanceof Error ? err.message : "spawn \u5931\u8D25", 500, err);
|
|
1677
|
+
queueMicrotask(() => this.emit("error", wrapped));
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
/**
|
|
1681
|
+
* 写入数据到 PTY stdin
|
|
1682
|
+
*
|
|
1683
|
+
* 进程未启动或已退出时静默丢弃(写日志),不抛错——
|
|
1684
|
+
* 因为客户端可能在 exit 通知到达前发出 user_input,硬抛会让代理崩溃
|
|
1685
|
+
*/
|
|
1686
|
+
write(data) {
|
|
1687
|
+
if (!this.process) {
|
|
1688
|
+
logger.warn({ dataLength: data.length }, "\u5C1D\u8BD5\u5199\u5165 PTY \u4F46\u8FDB\u7A0B\u672A\u8FD0\u884C");
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
this.process.write(data);
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* 调整 PTY 尺寸
|
|
1695
|
+
*
|
|
1696
|
+
* 同尺寸跳过——这是避免 resize 回环的关键
|
|
1697
|
+
*
|
|
1698
|
+
* 回环路径示例:
|
|
1699
|
+
* webapp resize → ws → PTY.resize → emit 'resize' → broadcast →
|
|
1700
|
+
* webapp 收到 terminal_resize → 触发 fit → 又算出同尺寸 → 再发 resize → ...
|
|
1701
|
+
* 同尺寸跳过让链路在第二步就断掉
|
|
1702
|
+
*/
|
|
1703
|
+
resize(cols, rows) {
|
|
1704
|
+
if (!this.process || this._exited)
|
|
1705
|
+
return;
|
|
1706
|
+
if (cols === this._cols && rows === this._rows) {
|
|
1707
|
+
logger.debug({ cols, rows }, "PTY resize \u8DF3\u8FC7\uFF08\u540C\u5C3A\u5BF8\uFF09");
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
logger.info({ cols, rows, prevCols: this._cols, prevRows: this._rows }, "PTY resize \u6267\u884C");
|
|
1711
|
+
try {
|
|
1712
|
+
this.process.resize(cols, rows);
|
|
1713
|
+
this._cols = cols;
|
|
1714
|
+
this._rows = rows;
|
|
1715
|
+
this.emit("resize", cols, rows);
|
|
1716
|
+
} catch (err) {
|
|
1717
|
+
logger.error({ err, cols, rows }, "PTY resize \u5931\u8D25");
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
/**
|
|
1721
|
+
* 销毁 PTY 进程
|
|
1722
|
+
*
|
|
1723
|
+
* 幂等:多次调用安全
|
|
1724
|
+
*/
|
|
1725
|
+
destroy() {
|
|
1726
|
+
if (!this.process)
|
|
1727
|
+
return;
|
|
1728
|
+
try {
|
|
1729
|
+
this.process.kill();
|
|
1730
|
+
logger.info("PTY \u8FDB\u7A0B\u5DF2 kill");
|
|
1731
|
+
} catch (err) {
|
|
1732
|
+
logger.error({ err }, "kill PTY \u8FDB\u7A0B\u5931\u8D25");
|
|
1733
|
+
}
|
|
1734
|
+
this.process = null;
|
|
1735
|
+
}
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
});
|
|
1739
|
+
|
|
1740
|
+
// backend/dist/ws/ws-server.js
|
|
1741
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
1742
|
+
var WsServer;
|
|
1743
|
+
var init_ws_server = __esm({
|
|
1744
|
+
"backend/dist/ws/ws-server.js"() {
|
|
1745
|
+
"use strict";
|
|
1746
|
+
init_dist();
|
|
1747
|
+
init_logger();
|
|
1748
|
+
WsServer = class {
|
|
1749
|
+
httpServer;
|
|
1750
|
+
opts;
|
|
1751
|
+
wss;
|
|
1752
|
+
clients = /* @__PURE__ */ new Set();
|
|
1753
|
+
upgradeTypes = /* @__PURE__ */ new WeakMap();
|
|
1754
|
+
heartbeatTimer = null;
|
|
1755
|
+
messageHandler = null;
|
|
1756
|
+
// onConnect / onDisconnect 升级为多 listener,多个独立模块(SessionController +
|
|
1757
|
+
// index.ts 的 spawn 触发器)需要同时监听新连接事件
|
|
1758
|
+
connectHandlers = [];
|
|
1759
|
+
disconnectHandlers = [];
|
|
1760
|
+
constructor(httpServer, opts = {}) {
|
|
1761
|
+
this.httpServer = httpServer;
|
|
1762
|
+
this.opts = opts;
|
|
1763
|
+
this.wss = new WebSocketServer({
|
|
1764
|
+
noServer: true,
|
|
1765
|
+
maxPayload: MAX_WS_MESSAGE_SIZE
|
|
1766
|
+
});
|
|
1767
|
+
this.setupUpgrade();
|
|
1768
|
+
this.setupConnection();
|
|
1769
|
+
this.startHeartbeat();
|
|
1770
|
+
}
|
|
1771
|
+
// ──────────────── 外部 hook 注入 ────────────────
|
|
1772
|
+
onMessage(fn) {
|
|
1773
|
+
this.messageHandler = fn;
|
|
1774
|
+
}
|
|
1775
|
+
/** 注册新连接 listener;可重复调用,多 listener 独立触发 */
|
|
1776
|
+
onConnect(fn) {
|
|
1777
|
+
this.connectHandlers.push(fn);
|
|
1778
|
+
}
|
|
1779
|
+
/** 注册断开 listener;可重复调用 */
|
|
1780
|
+
onDisconnect(fn) {
|
|
1781
|
+
this.disconnectHandlers.push(fn);
|
|
1782
|
+
}
|
|
1783
|
+
// ──────────────── 客户端统计 ────────────────
|
|
1784
|
+
get clientCount() {
|
|
1785
|
+
return this.clients.size;
|
|
1786
|
+
}
|
|
1787
|
+
getClientCounts() {
|
|
1788
|
+
let webapp = 0;
|
|
1789
|
+
let attach = 0;
|
|
1790
|
+
for (const c of this.clients) {
|
|
1791
|
+
if (c.clientType === "attach")
|
|
1792
|
+
attach++;
|
|
1793
|
+
else
|
|
1794
|
+
webapp++;
|
|
1795
|
+
}
|
|
1796
|
+
return { webapp, attach };
|
|
1797
|
+
}
|
|
1798
|
+
// ──────────────── 发送 ────────────────
|
|
1799
|
+
/** 广播到所有连接的客户端 */
|
|
1800
|
+
broadcast(msg) {
|
|
1801
|
+
const payload = JSON.stringify(msg);
|
|
1802
|
+
for (const c of this.clients) {
|
|
1803
|
+
if (c.ws.readyState === WebSocket.OPEN) {
|
|
1804
|
+
c.ws.send(payload);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
/** 发送给指定 WebSocket */
|
|
1809
|
+
sendTo(ws, msg) {
|
|
1810
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1811
|
+
ws.send(JSON.stringify(msg));
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
// ──────────────── 关闭 ────────────────
|
|
1815
|
+
destroy() {
|
|
1816
|
+
if (this.heartbeatTimer) {
|
|
1817
|
+
clearInterval(this.heartbeatTimer);
|
|
1818
|
+
this.heartbeatTimer = null;
|
|
1819
|
+
}
|
|
1820
|
+
for (const c of this.clients) {
|
|
1821
|
+
c.ws.terminate();
|
|
1822
|
+
}
|
|
1823
|
+
this.clients.clear();
|
|
1824
|
+
this.wss.close();
|
|
1825
|
+
}
|
|
1826
|
+
// ──────────────── 内部:upgrade 路径鉴权 ────────────────
|
|
1827
|
+
setupUpgrade() {
|
|
1828
|
+
this.httpServer.on("upgrade", (req, socket, head) => {
|
|
1829
|
+
let pathname = "/";
|
|
1830
|
+
try {
|
|
1831
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
1832
|
+
pathname = url.pathname;
|
|
1833
|
+
} catch {
|
|
1834
|
+
socket.destroy();
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
if (pathname !== "/ws") {
|
|
1838
|
+
socket.destroy();
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
const clientType = this.opts.authenticate ? this.opts.authenticate(req) : "webapp";
|
|
1842
|
+
if (clientType === null) {
|
|
1843
|
+
logger.warn({ url: req.url }, "WS upgrade \u88AB\u9274\u6743\u62D2\u7EDD");
|
|
1844
|
+
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
|
1845
|
+
socket.destroy();
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
this.upgradeTypes.set(req, clientType);
|
|
1849
|
+
this.wss.handleUpgrade(req, socket, head, (ws) => {
|
|
1850
|
+
this.wss.emit("connection", ws, req);
|
|
1851
|
+
});
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1854
|
+
// ──────────────── 内部:connection 处理 ────────────────
|
|
1855
|
+
setupConnection() {
|
|
1856
|
+
this.wss.on("connection", (ws, req) => {
|
|
1857
|
+
const clientType = this.upgradeTypes.get(req) ?? "webapp";
|
|
1858
|
+
const entry = { ws, alive: true, clientType };
|
|
1859
|
+
this.clients.add(entry);
|
|
1860
|
+
logger.info({ total: this.clients.size, clientType }, "WS \u5BA2\u6237\u7AEF\u5DF2\u8FDE\u63A5");
|
|
1861
|
+
ws.on("pong", () => {
|
|
1862
|
+
entry.alive = true;
|
|
1863
|
+
});
|
|
1864
|
+
ws.on("message", (raw) => {
|
|
1865
|
+
if (this.messageHandler) {
|
|
1866
|
+
this.messageHandler(ws, raw.toString(), clientType);
|
|
1867
|
+
}
|
|
1868
|
+
});
|
|
1869
|
+
const removeAndNotify = () => {
|
|
1870
|
+
if (this.clients.delete(entry)) {
|
|
1871
|
+
const counts = this.getClientCounts();
|
|
1872
|
+
logger.info({ total: this.clients.size, ...counts }, "WS \u5BA2\u6237\u7AEF\u5DF2\u65AD\u5F00");
|
|
1873
|
+
for (const fn of this.disconnectHandlers)
|
|
1874
|
+
fn(counts);
|
|
1875
|
+
}
|
|
1876
|
+
};
|
|
1877
|
+
ws.on("close", removeAndNotify);
|
|
1878
|
+
ws.on("error", (err) => {
|
|
1879
|
+
logger.warn({ err, clientType }, "WS \u5BA2\u6237\u7AEF\u9519\u8BEF");
|
|
1880
|
+
removeAndNotify();
|
|
1881
|
+
});
|
|
1882
|
+
for (const fn of this.connectHandlers)
|
|
1883
|
+
fn(ws, clientType);
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
// ──────────────── 内部:心跳 ────────────────
|
|
1887
|
+
startHeartbeat() {
|
|
1888
|
+
this.heartbeatTimer = setInterval(() => {
|
|
1889
|
+
for (const c of this.clients) {
|
|
1890
|
+
if (!c.alive) {
|
|
1891
|
+
logger.info("\u5FC3\u8DF3\u8D85\u65F6\uFF0Cterminate \u65E0\u54CD\u5E94\u5BA2\u6237\u7AEF");
|
|
1892
|
+
c.ws.terminate();
|
|
1893
|
+
this.clients.delete(c);
|
|
1894
|
+
continue;
|
|
1895
|
+
}
|
|
1896
|
+
c.alive = false;
|
|
1897
|
+
c.ws.ping();
|
|
1898
|
+
}
|
|
1899
|
+
}, WS_HEARTBEAT_INTERVAL_MS);
|
|
1900
|
+
if (typeof this.heartbeatTimer.unref === "function") {
|
|
1901
|
+
this.heartbeatTimer.unref();
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
});
|
|
1907
|
+
|
|
1908
|
+
// backend/dist/pty/output-buffer.js
|
|
1909
|
+
var OutputBuffer;
|
|
1910
|
+
var init_output_buffer = __esm({
|
|
1911
|
+
"backend/dist/pty/output-buffer.js"() {
|
|
1912
|
+
"use strict";
|
|
1913
|
+
OutputBuffer = class {
|
|
1914
|
+
lines = [];
|
|
1915
|
+
/** 最后一段不完整的行(未碰到 \n 时累积) */
|
|
1916
|
+
partial = "";
|
|
1917
|
+
/** 单调递增版本号 */
|
|
1918
|
+
seq = 0;
|
|
1919
|
+
/** 行数上限(超过 maxLines × 1.1 时裁剪) */
|
|
1920
|
+
maxLines;
|
|
1921
|
+
/**
|
|
1922
|
+
* @param maxLines 缓冲区最大行数(默认 10000)
|
|
1923
|
+
*/
|
|
1924
|
+
constructor(maxLines = 1e4) {
|
|
1925
|
+
if (!Number.isInteger(maxLines) || maxLines <= 0) {
|
|
1926
|
+
throw new Error("OutputBuffer: maxLines \u5FC5\u987B\u662F\u6B63\u6574\u6570");
|
|
1927
|
+
}
|
|
1928
|
+
this.maxLines = maxLines;
|
|
1929
|
+
}
|
|
1930
|
+
/** 当前版本号 */
|
|
1931
|
+
get sequenceNumber() {
|
|
1932
|
+
return this.seq;
|
|
1933
|
+
}
|
|
1934
|
+
/** 当前缓冲行数(不含 partial) */
|
|
1935
|
+
get lineCount() {
|
|
1936
|
+
return this.lines.length;
|
|
1937
|
+
}
|
|
1938
|
+
/**
|
|
1939
|
+
* 追加 PTY 输出片段
|
|
1940
|
+
*
|
|
1941
|
+
* 输入可能:
|
|
1942
|
+
* - 不含 \n(全部并入 partial)
|
|
1943
|
+
* - 含一个或多个 \n(分割后前面入 lines,最后片段入 partial)
|
|
1944
|
+
* - 以 \n 结尾(最后片段为空,partial 重置为空字符串)
|
|
1945
|
+
*/
|
|
1946
|
+
append(data) {
|
|
1947
|
+
this.seq++;
|
|
1948
|
+
if (data.length === 0)
|
|
1949
|
+
return;
|
|
1950
|
+
const combined = this.partial + data;
|
|
1951
|
+
const parts = combined.split("\n");
|
|
1952
|
+
this.partial = parts.pop() ?? "";
|
|
1953
|
+
if (parts.length > 0) {
|
|
1954
|
+
for (const line of parts) {
|
|
1955
|
+
this.lines.push(line);
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
const trimThreshold = Math.floor(this.maxLines * 1.1);
|
|
1959
|
+
if (this.lines.length > trimThreshold) {
|
|
1960
|
+
this.lines = this.lines.slice(this.lines.length - this.maxLines);
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
/**
|
|
1964
|
+
* 获取重建后的完整缓冲内容(含已落定的行 + 尾部 partial)
|
|
1965
|
+
*
|
|
1966
|
+
* 输出形式:
|
|
1967
|
+
* - 无内容时返回 ''
|
|
1968
|
+
* - 仅 partial 时返回 partial(无尾随 \n)
|
|
1969
|
+
* - 仅完整行时每行后跟 \n(保留原始流结构)
|
|
1970
|
+
* - 既有完整行又有 partial 时:行间 \n + 尾部 partial(无尾随 \n)
|
|
1971
|
+
*
|
|
1972
|
+
* 设计目的:让客户端 xterm.js 接收 history_sync.data 后,
|
|
1973
|
+
* 直接 term.write(data) 即可还原与实时一致的画面
|
|
1974
|
+
*/
|
|
1975
|
+
getFullContent() {
|
|
1976
|
+
if (this.lines.length === 0) {
|
|
1977
|
+
return this.partial;
|
|
1978
|
+
}
|
|
1979
|
+
const joined = this.lines.join("\n") + "\n";
|
|
1980
|
+
return this.partial.length > 0 ? joined + this.partial : joined;
|
|
1981
|
+
}
|
|
1982
|
+
/** 清空所有内容(保留 seq——seq 是版本戳不重置) */
|
|
1983
|
+
clear() {
|
|
1984
|
+
this.lines = [];
|
|
1985
|
+
this.partial = "";
|
|
1986
|
+
}
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
});
|
|
1990
|
+
|
|
1991
|
+
// backend/dist/ws/ws-handler.js
|
|
1992
|
+
import { WebSocket as WebSocket2 } from "ws";
|
|
1993
|
+
function handleWsMessage(ws, raw, cb) {
|
|
1994
|
+
let msg;
|
|
1995
|
+
try {
|
|
1996
|
+
msg = JSON.parse(raw);
|
|
1997
|
+
} catch {
|
|
1998
|
+
logger.warn({ rawSnippet: raw.slice(0, 200) }, "\u6536\u5230\u975E\u6CD5 JSON WS \u6D88\u606F");
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
switch (msg.type) {
|
|
2002
|
+
case "user_input":
|
|
2003
|
+
if (typeof msg.data === "string") {
|
|
2004
|
+
cb.onUserInput(msg.data);
|
|
2005
|
+
} else {
|
|
2006
|
+
logger.warn({ msg }, "user_input \u7F3A data \u5B57\u6BB5\u6216\u7C7B\u578B\u9519\u8BEF");
|
|
2007
|
+
}
|
|
2008
|
+
break;
|
|
2009
|
+
case "resize":
|
|
2010
|
+
if (typeof msg.cols === "number" && typeof msg.rows === "number") {
|
|
2011
|
+
cb.onResize(msg.cols, msg.rows);
|
|
2012
|
+
} else {
|
|
2013
|
+
logger.warn({ msg }, "resize \u7F3A cols/rows \u5B57\u6BB5\u6216\u7C7B\u578B\u9519\u8BEF");
|
|
2014
|
+
}
|
|
2015
|
+
break;
|
|
2016
|
+
case "heartbeat":
|
|
2017
|
+
if (ws.readyState === WebSocket2.OPEN) {
|
|
2018
|
+
ws.send(JSON.stringify({ type: "heartbeat", timestamp: Date.now() }));
|
|
2019
|
+
}
|
|
2020
|
+
break;
|
|
2021
|
+
default: {
|
|
2022
|
+
const t = msg.type;
|
|
2023
|
+
logger.warn({ type: t }, "\u672A\u77E5 WS \u6D88\u606F\u7C7B\u578B\uFF0C\u5DF2\u5FFD\u7565");
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
var init_ws_handler = __esm({
|
|
2028
|
+
"backend/dist/ws/ws-handler.js"() {
|
|
2029
|
+
"use strict";
|
|
2030
|
+
init_logger();
|
|
2031
|
+
}
|
|
2032
|
+
});
|
|
2033
|
+
|
|
2034
|
+
// backend/dist/utils/ansi-filter.js
|
|
2035
|
+
var ALT_ENTER, ALT_EXIT, AnsiFilter;
|
|
2036
|
+
var init_ansi_filter = __esm({
|
|
2037
|
+
"backend/dist/utils/ansi-filter.js"() {
|
|
2038
|
+
"use strict";
|
|
2039
|
+
ALT_ENTER = "\x1B[?1049h";
|
|
2040
|
+
ALT_EXIT = "\x1B[?1049l";
|
|
2041
|
+
AnsiFilter = class {
|
|
2042
|
+
mode = "normal";
|
|
2043
|
+
/** 跨 chunk 拼接的前缀缓冲(最长保留 ALT_ENTER 长度 - 1) */
|
|
2044
|
+
pending = "";
|
|
2045
|
+
/** 当前是否在 alt screen 内 */
|
|
2046
|
+
get currentMode() {
|
|
2047
|
+
return this.mode;
|
|
2048
|
+
}
|
|
2049
|
+
/**
|
|
2050
|
+
* 处理一段 PTY 输出,返回应该被广播 / 写入 buffer 的部分
|
|
2051
|
+
*
|
|
2052
|
+
* 行为:
|
|
2053
|
+
* - 在 normal 模式下:返回 chunk(含 ALT_ENTER 序列本身),切到 alt 模式
|
|
2054
|
+
* - 在 alt 模式下:丢弃所有内容,仅返回 ALT_EXIT 序列本身(让前端 xterm
|
|
2055
|
+
* 退出 alt screen),切回 normal
|
|
2056
|
+
*/
|
|
2057
|
+
filter(chunk) {
|
|
2058
|
+
let input = this.pending + chunk;
|
|
2059
|
+
this.pending = "";
|
|
2060
|
+
let output = "";
|
|
2061
|
+
let i = 0;
|
|
2062
|
+
while (i < input.length) {
|
|
2063
|
+
if (this.mode === "normal") {
|
|
2064
|
+
const idx = input.indexOf(ALT_ENTER, i);
|
|
2065
|
+
if (idx === -1) {
|
|
2066
|
+
output += this.cutTrailingEsc(input.slice(i));
|
|
2067
|
+
break;
|
|
2068
|
+
}
|
|
2069
|
+
output += input.slice(i, idx + ALT_ENTER.length);
|
|
2070
|
+
i = idx + ALT_ENTER.length;
|
|
2071
|
+
this.mode = "alt";
|
|
2072
|
+
} else {
|
|
2073
|
+
const idx = input.indexOf(ALT_EXIT, i);
|
|
2074
|
+
if (idx === -1) {
|
|
2075
|
+
this.pending = this.captureTrailingEsc(input.slice(i));
|
|
2076
|
+
break;
|
|
2077
|
+
}
|
|
2078
|
+
output += ALT_EXIT;
|
|
2079
|
+
i = idx + ALT_EXIT.length;
|
|
2080
|
+
this.mode = "normal";
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
return output;
|
|
2084
|
+
}
|
|
2085
|
+
/**
|
|
2086
|
+
* 重置状态(用于会话结束等)
|
|
2087
|
+
*/
|
|
2088
|
+
reset() {
|
|
2089
|
+
this.mode = "normal";
|
|
2090
|
+
this.pending = "";
|
|
2091
|
+
}
|
|
2092
|
+
/**
|
|
2093
|
+
* 把字符串末尾"看起来像未完成的 ALT_ENTER 前缀"截下来挂 pending
|
|
2094
|
+
*
|
|
2095
|
+
* 示例:'foo\x1b[?1049' 截掉 '\x1b[?1049' 留作下次拼接,否则下一 chunk
|
|
2096
|
+
* 来个 'h' 就接不上了。
|
|
2097
|
+
*
|
|
2098
|
+
* 返回的是"前面那段安全可以输出的内容"。
|
|
2099
|
+
*/
|
|
2100
|
+
cutTrailingEsc(s) {
|
|
2101
|
+
const last = s.lastIndexOf("\x1B");
|
|
2102
|
+
if (last === -1)
|
|
2103
|
+
return s;
|
|
2104
|
+
const tail = s.slice(last);
|
|
2105
|
+
if (ALT_ENTER.startsWith(tail) && tail !== ALT_ENTER) {
|
|
2106
|
+
this.pending = tail;
|
|
2107
|
+
return s.slice(0, last);
|
|
2108
|
+
}
|
|
2109
|
+
return s;
|
|
2110
|
+
}
|
|
2111
|
+
/** alt 模式下用:保留 ALT_EXIT 前缀,否则空字符串(其它内容已被丢弃) */
|
|
2112
|
+
captureTrailingEsc(s) {
|
|
2113
|
+
const last = s.lastIndexOf("\x1B");
|
|
2114
|
+
if (last === -1)
|
|
2115
|
+
return "";
|
|
2116
|
+
const tail = s.slice(last);
|
|
2117
|
+
if (ALT_EXIT.startsWith(tail) && tail !== ALT_EXIT) {
|
|
2118
|
+
return tail;
|
|
2119
|
+
}
|
|
2120
|
+
return "";
|
|
2121
|
+
}
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
});
|
|
2125
|
+
|
|
2126
|
+
// backend/dist/session/session-controller.js
|
|
2127
|
+
var SessionController;
|
|
2128
|
+
var init_session_controller = __esm({
|
|
2129
|
+
"backend/dist/session/session-controller.js"() {
|
|
2130
|
+
"use strict";
|
|
2131
|
+
init_output_buffer();
|
|
2132
|
+
init_ws_handler();
|
|
2133
|
+
init_ansi_filter();
|
|
2134
|
+
init_logger();
|
|
2135
|
+
init_constants2();
|
|
2136
|
+
SessionController = class {
|
|
2137
|
+
pty;
|
|
2138
|
+
ws;
|
|
2139
|
+
// ──────────── 状态 ────────────
|
|
2140
|
+
// 初始化为 pty_pending:listen 已就绪,但 PTY 子进程还没 spawn。
|
|
2141
|
+
// index.ts 的 spawn 触发器命中后会 setStatus('running');spawn 失败再 fallback 到 idle。
|
|
2142
|
+
_status = "pty_pending";
|
|
2143
|
+
buffer;
|
|
2144
|
+
writeToProcessStdout;
|
|
2145
|
+
// ──────────── WS 输出批合并 ────────────
|
|
2146
|
+
/** 待 flush 的输出片段队列 */
|
|
2147
|
+
wsPendingChunks = [];
|
|
2148
|
+
/** 待 flush 的总字节数 */
|
|
2149
|
+
wsPendingBytes = 0;
|
|
2150
|
+
/** 16ms 时间窗 timer */
|
|
2151
|
+
wsFlushTimer = null;
|
|
2152
|
+
// ──────────── 调试统计(PTY 退出时打印) ────────────
|
|
2153
|
+
ptyTotalBytes = 0;
|
|
2154
|
+
wsFlushCount = 0;
|
|
2155
|
+
wsFlushBytesTotal = 0;
|
|
2156
|
+
wsMaxPendingBytes = 0;
|
|
2157
|
+
wsBackpressureEvents = 0;
|
|
2158
|
+
/** 可选的 hook 接收器(阶段 3 启用) */
|
|
2159
|
+
hookReceiver = null;
|
|
2160
|
+
/** ANSI 过滤器(阶段 8 启用;null = 关闭过滤直接透传) */
|
|
2161
|
+
ansiFilter;
|
|
2162
|
+
/** 可选的 PushService(阶段 9 启用;用于 hook 触发时推送通知) */
|
|
2163
|
+
pushService = null;
|
|
2164
|
+
/** 实例显示名 + 端口(用于推送 payload;阶段 9 启用) */
|
|
2165
|
+
pushContext = null;
|
|
2166
|
+
constructor(pty2, ws, maxBufferLines, opts = {}) {
|
|
2167
|
+
this.pty = pty2;
|
|
2168
|
+
this.ws = ws;
|
|
2169
|
+
this.buffer = new OutputBuffer(maxBufferLines);
|
|
2170
|
+
this.writeToProcessStdout = opts.writeToProcessStdout ?? true;
|
|
2171
|
+
this.ansiFilter = opts.ansiFilter ?? true ? new AnsiFilter() : null;
|
|
2172
|
+
this.wirePty();
|
|
2173
|
+
this.wireWs();
|
|
2174
|
+
}
|
|
2175
|
+
/**
|
|
2176
|
+
* 注入 HookReceiver
|
|
2177
|
+
*
|
|
2178
|
+
* 设计为 setter 而非构造参数,因为 HookReceiver 只在阶段 3+ 启用,
|
|
2179
|
+
* 且阶段 6a Web 创建实例的 headless 模式可能不需要它。
|
|
2180
|
+
*/
|
|
2181
|
+
setHookReceiver(receiver) {
|
|
2182
|
+
if (this.hookReceiver) {
|
|
2183
|
+
logger.warn("SessionController.setHookReceiver \u91CD\u590D\u8C03\u7528\uFF0C\u8986\u76D6\u65E7 receiver");
|
|
2184
|
+
}
|
|
2185
|
+
this.hookReceiver = receiver;
|
|
2186
|
+
receiver.on("notification", (notif) => this.onHookNotification(notif));
|
|
2187
|
+
}
|
|
2188
|
+
/**
|
|
2189
|
+
* 注入 PushService(阶段 9)
|
|
2190
|
+
*
|
|
2191
|
+
* @param push PushService 实例(应已 init)
|
|
2192
|
+
* @param context 实例标识,用于推送 payload 中的 title/url
|
|
2193
|
+
*/
|
|
2194
|
+
setPushService(push, context) {
|
|
2195
|
+
this.pushService = push;
|
|
2196
|
+
this.pushContext = context;
|
|
2197
|
+
}
|
|
2198
|
+
/**
|
|
2199
|
+
* 处理 hook 触发的审批通知
|
|
2200
|
+
*
|
|
2201
|
+
* - 状态切到 waiting_input
|
|
2202
|
+
* - 广播 status_update 让前端 StatusBar 显示警告色
|
|
2203
|
+
* - detail 字段附加工具名让用户知道是哪个工具在等
|
|
2204
|
+
*
|
|
2205
|
+
* 不直接广播文本提示——审批 prompt 已经通过 PTY 输出到 xterm 显示了,
|
|
2206
|
+
* 用户在 xterm 内输入 y/Esc 即可
|
|
2207
|
+
*/
|
|
2208
|
+
onHookNotification(notif) {
|
|
2209
|
+
logger.info({ tool: notif.tool }, "\u5BA1\u6279\u901A\u77E5\u5230\u8FBE\uFF0C\u5207\u5230 waiting_input");
|
|
2210
|
+
this._status = "waiting_input";
|
|
2211
|
+
this.ws.broadcast({
|
|
2212
|
+
type: "status_update",
|
|
2213
|
+
status: "waiting_input",
|
|
2214
|
+
detail: `\u7B49\u5F85\u5BA1\u6279\uFF1A${notif.tool}`
|
|
2215
|
+
});
|
|
2216
|
+
if (this.pushService && this.pushContext) {
|
|
2217
|
+
const ctx = this.pushContext;
|
|
2218
|
+
const title = `[${ctx.instanceName}] Claude \u7B49\u5F85\u5BA1\u6279`;
|
|
2219
|
+
const body = `\u5DE5\u5177\uFF1A${notif.tool}`;
|
|
2220
|
+
void this.pushService.notifyAll({ title, body, url: ctx.url }).catch((err) => logger.warn({ err }, "\u63A8\u9001 hook \u901A\u77E5\u5931\u8D25"));
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
// ──────────────── 公共 API ────────────────
|
|
2224
|
+
get status() {
|
|
2225
|
+
return this._status;
|
|
2226
|
+
}
|
|
2227
|
+
get connectedClients() {
|
|
2228
|
+
return this.ws.clientCount;
|
|
2229
|
+
}
|
|
2230
|
+
/**
|
|
2231
|
+
* 主动设置状态并广播给所有客户端
|
|
2232
|
+
*
|
|
2233
|
+
* 例如服务启动后将 idle → running,PTY 退出时 running → idle
|
|
2234
|
+
*/
|
|
2235
|
+
setStatus(status, detail) {
|
|
2236
|
+
this._status = status;
|
|
2237
|
+
this.ws.broadcast({ type: "status_update", status, ...detail ? { detail } : {} });
|
|
2238
|
+
}
|
|
2239
|
+
/**
|
|
2240
|
+
* 析构:清理批合并 timer,最后一次 flush
|
|
2241
|
+
*
|
|
2242
|
+
* 在 SIGTERM / SIGINT 优雅关闭时调用
|
|
2243
|
+
*/
|
|
2244
|
+
destroy() {
|
|
2245
|
+
this.flushPendingWsOutput();
|
|
2246
|
+
}
|
|
2247
|
+
// ──────────────── PTY → 三向分发 ────────────────
|
|
2248
|
+
wirePty() {
|
|
2249
|
+
this.pty.on("data", (data) => {
|
|
2250
|
+
this.ptyTotalBytes += Buffer.byteLength(data, "utf8");
|
|
2251
|
+
if (this.writeToProcessStdout) {
|
|
2252
|
+
process.stdout.write(data);
|
|
2253
|
+
}
|
|
2254
|
+
const filtered = this.ansiFilter ? this.ansiFilter.filter(data) : data;
|
|
2255
|
+
if (filtered.length === 0)
|
|
2256
|
+
return;
|
|
2257
|
+
this.buffer.append(filtered);
|
|
2258
|
+
this.enqueueWsOutput(filtered);
|
|
2259
|
+
});
|
|
2260
|
+
this.pty.on("exit", (exitCode) => {
|
|
2261
|
+
this.flushPendingWsOutput();
|
|
2262
|
+
this._status = "idle";
|
|
2263
|
+
this.ws.broadcast({
|
|
2264
|
+
type: "session_ended",
|
|
2265
|
+
exitCode,
|
|
2266
|
+
reason: exitCode === 0 ? "Process exited normally" : `Process exited with code ${exitCode}`
|
|
2267
|
+
});
|
|
2268
|
+
logger.info({
|
|
2269
|
+
exitCode,
|
|
2270
|
+
ptyTotalBytes: this.ptyTotalBytes,
|
|
2271
|
+
wsFlushCount: this.wsFlushCount,
|
|
2272
|
+
wsFlushBytesTotal: this.wsFlushBytesTotal,
|
|
2273
|
+
wsMaxPendingBytes: this.wsMaxPendingBytes,
|
|
2274
|
+
wsBackpressureEvents: this.wsBackpressureEvents
|
|
2275
|
+
}, "\u4F1A\u8BDD\u7ED3\u675F");
|
|
2276
|
+
});
|
|
2277
|
+
this.pty.on("error", (err) => {
|
|
2278
|
+
logger.error({ err }, "PTY \u9519\u8BEF");
|
|
2279
|
+
this.ws.broadcast({
|
|
2280
|
+
type: "error",
|
|
2281
|
+
code: "pty_error",
|
|
2282
|
+
message: err.message
|
|
2283
|
+
});
|
|
2284
|
+
});
|
|
2285
|
+
this.pty.on("resize", (cols, rows) => {
|
|
2286
|
+
this.ws.broadcast({ type: "terminal_resize", cols, rows });
|
|
2287
|
+
});
|
|
2288
|
+
}
|
|
2289
|
+
/**
|
|
2290
|
+
* 入队一段 PTY 输出,按三阈值决定是否立即 flush
|
|
2291
|
+
*/
|
|
2292
|
+
enqueueWsOutput(data) {
|
|
2293
|
+
this.wsPendingChunks.push(data);
|
|
2294
|
+
this.wsPendingBytes += Buffer.byteLength(data, "utf8");
|
|
2295
|
+
if (this.wsPendingBytes > this.wsMaxPendingBytes) {
|
|
2296
|
+
this.wsMaxPendingBytes = this.wsPendingBytes;
|
|
2297
|
+
}
|
|
2298
|
+
if (this.wsPendingBytes >= WS_HIGH_WATERMARK_BYTES) {
|
|
2299
|
+
this.wsBackpressureEvents++;
|
|
2300
|
+
this.flushPendingWsOutput();
|
|
2301
|
+
return;
|
|
2302
|
+
}
|
|
2303
|
+
if (this.wsPendingBytes >= WS_MAX_CHUNK_BYTES) {
|
|
2304
|
+
this.flushPendingWsOutput();
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
2307
|
+
if (!this.wsFlushTimer) {
|
|
2308
|
+
this.wsFlushTimer = setTimeout(() => {
|
|
2309
|
+
this.wsFlushTimer = null;
|
|
2310
|
+
this.flushPendingWsOutput();
|
|
2311
|
+
}, WS_FLUSH_INTERVAL_MS);
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
/**
|
|
2315
|
+
* 把 pending 的输出合并成一条 terminal_output 消息广播
|
|
2316
|
+
*/
|
|
2317
|
+
flushPendingWsOutput() {
|
|
2318
|
+
if (this.wsFlushTimer) {
|
|
2319
|
+
clearTimeout(this.wsFlushTimer);
|
|
2320
|
+
this.wsFlushTimer = null;
|
|
2321
|
+
}
|
|
2322
|
+
if (this.wsPendingChunks.length === 0)
|
|
2323
|
+
return;
|
|
2324
|
+
const merged = this.wsPendingChunks.join("");
|
|
2325
|
+
const bytes = this.wsPendingBytes;
|
|
2326
|
+
this.wsPendingChunks = [];
|
|
2327
|
+
this.wsPendingBytes = 0;
|
|
2328
|
+
this.wsFlushCount++;
|
|
2329
|
+
this.wsFlushBytesTotal += bytes;
|
|
2330
|
+
this.ws.broadcast({
|
|
2331
|
+
type: "terminal_output",
|
|
2332
|
+
data: merged,
|
|
2333
|
+
seq: this.buffer.sequenceNumber
|
|
2334
|
+
});
|
|
2335
|
+
}
|
|
2336
|
+
// ──────────────── WS → PTY ────────────────
|
|
2337
|
+
wireWs() {
|
|
2338
|
+
this.ws.onMessage((wsConn, raw, type) => {
|
|
2339
|
+
handleWsMessage(wsConn, raw, {
|
|
2340
|
+
onUserInput: (data) => {
|
|
2341
|
+
this.pty.write(data);
|
|
2342
|
+
},
|
|
2343
|
+
onResize: (cols, rows) => {
|
|
2344
|
+
const counts = this.ws.getClientCounts();
|
|
2345
|
+
if (counts.webapp > 0 && type === "attach") {
|
|
2346
|
+
logger.debug({ type, cols, rows, counts }, "webapp \u5728\u7EBF\uFF0Cattach \u7684 resize \u88AB\u5FFD\u7565");
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
this.pty.resize(cols, rows);
|
|
2350
|
+
}
|
|
2351
|
+
});
|
|
2352
|
+
});
|
|
2353
|
+
this.ws.onConnect((wsConn, type) => {
|
|
2354
|
+
logger.info({ clientType: type }, "\u65B0\u5BA2\u6237\u7AEF\u8FDE\u5165\uFF0C\u63A8\u9001 history_sync");
|
|
2355
|
+
this.ws.sendTo(wsConn, {
|
|
2356
|
+
type: "history_sync",
|
|
2357
|
+
data: this.buffer.getFullContent(),
|
|
2358
|
+
seq: this.buffer.sequenceNumber,
|
|
2359
|
+
status: this._status,
|
|
2360
|
+
cols: this.pty.cols,
|
|
2361
|
+
rows: this.pty.rows
|
|
2362
|
+
});
|
|
2363
|
+
});
|
|
2364
|
+
this.ws.onDisconnect((counts) => {
|
|
2365
|
+
logger.debug(counts, "\u5BA2\u6237\u7AEF\u65AD\u5F00\u540E\u5269\u4F59\u7EDF\u8BA1");
|
|
2366
|
+
if (counts.webapp === 0 && counts.attach > 0) {
|
|
2367
|
+
this.ws.broadcast({
|
|
2368
|
+
type: "terminal_resize",
|
|
2369
|
+
cols: this.pty.cols,
|
|
2370
|
+
rows: this.pty.rows
|
|
2371
|
+
});
|
|
2372
|
+
}
|
|
2373
|
+
});
|
|
2374
|
+
}
|
|
2375
|
+
};
|
|
2376
|
+
}
|
|
2377
|
+
});
|
|
2378
|
+
|
|
2379
|
+
// backend/dist/terminal/terminal-relay.js
|
|
2380
|
+
var CTRL_C_BYTE, TERM_RESET_SEQ, KITTY_CTRL_C_RE, TerminalRelay;
|
|
2381
|
+
var init_terminal_relay = __esm({
|
|
2382
|
+
"backend/dist/terminal/terminal-relay.js"() {
|
|
2383
|
+
"use strict";
|
|
2384
|
+
init_logger();
|
|
2385
|
+
init_constants2();
|
|
2386
|
+
CTRL_C_BYTE = 3;
|
|
2387
|
+
TERM_RESET_SEQ = "\x1B[?1l\x1B[?9l\x1B[?1000l\x1B[?1001l\x1B[?1002l\x1B[?1003l\x1B[?1004l\x1B[?1005l\x1B[?1006l\x1B[?1015l\x1B[?2004l\x1B[?1049l\x1B[?25h\x1B[m";
|
|
2388
|
+
KITTY_CTRL_C_RE = /\x1b\[99;5(?::(?:[12]))?(?:;\d+)*u/;
|
|
2389
|
+
TerminalRelay = class {
|
|
2390
|
+
pty;
|
|
2391
|
+
opts;
|
|
2392
|
+
stdinHandler = null;
|
|
2393
|
+
resizeHandler = null;
|
|
2394
|
+
wasRaw = false;
|
|
2395
|
+
resizePaused = false;
|
|
2396
|
+
lastCtrlCAt = 0;
|
|
2397
|
+
started = false;
|
|
2398
|
+
constructor(pty2, opts = {}) {
|
|
2399
|
+
this.pty = pty2;
|
|
2400
|
+
this.opts = opts;
|
|
2401
|
+
}
|
|
2402
|
+
/**
|
|
2403
|
+
* 启动 raw mode 透传
|
|
2404
|
+
*
|
|
2405
|
+
* 非 TTY 环境(pnpm dev 通过管道、CI 等)下也能调用:
|
|
2406
|
+
* - 不会调用 setRawMode(不存在)
|
|
2407
|
+
* - 仅监听 stdin 'data' 事件做 Ctrl+C 检测和透传
|
|
2408
|
+
*/
|
|
2409
|
+
start() {
|
|
2410
|
+
if (this.started)
|
|
2411
|
+
return;
|
|
2412
|
+
this.started = true;
|
|
2413
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
2414
|
+
this.wasRaw = process.stdin.isRaw === true;
|
|
2415
|
+
process.stdin.setRawMode(true);
|
|
2416
|
+
logger.debug("stdin \u8FDB\u5165 raw mode");
|
|
2417
|
+
} else {
|
|
2418
|
+
logger.warn("stdin \u4E0D\u662F TTY\uFF0CTerminalRelay \u8DF3\u8FC7 setRawMode");
|
|
2419
|
+
}
|
|
2420
|
+
process.stdin.resume();
|
|
2421
|
+
process.stdin.setEncoding("utf8");
|
|
2422
|
+
this.stdinHandler = (chunk) => this.handleStdin(chunk);
|
|
2423
|
+
process.stdin.on("data", this.stdinHandler);
|
|
2424
|
+
if (process.stdout.isTTY) {
|
|
2425
|
+
this.resizeHandler = () => this.handleResize();
|
|
2426
|
+
process.stdout.on("resize", this.resizeHandler);
|
|
2427
|
+
this.handleResize();
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
/**
|
|
2431
|
+
* 停止透传,恢复 stdin + 复位本地终端模式
|
|
2432
|
+
*
|
|
2433
|
+
* 为什么需要复位本地终端模式:
|
|
2434
|
+
* PTY 子进程(claude / vim / htop / fzf 等)通过 escape 序列改了**本地终端**的
|
|
2435
|
+
* 状态——开启鼠标追踪、切到 alt-screen、应用键盘模式、bracketed paste、隐藏光标。
|
|
2436
|
+
* 这些状态写在 termios / 终端 emulator 里,PTY 退出时**不会自动还原**。
|
|
2437
|
+
* 不复位的后果(用户实测):atr 退出后本地终端
|
|
2438
|
+
* - 鼠标移动 / 点击被解析成坐标字符串疯狂 echo
|
|
2439
|
+
* - 终端"看似无响应"(实则在 alt-screen 里)
|
|
2440
|
+
* - 方向键发的序列错位
|
|
2441
|
+
*
|
|
2442
|
+
* 幂等
|
|
2443
|
+
*/
|
|
2444
|
+
stop() {
|
|
2445
|
+
if (!this.started)
|
|
2446
|
+
return;
|
|
2447
|
+
this.started = false;
|
|
2448
|
+
if (this.stdinHandler) {
|
|
2449
|
+
process.stdin.off("data", this.stdinHandler);
|
|
2450
|
+
this.stdinHandler = null;
|
|
2451
|
+
}
|
|
2452
|
+
if (this.resizeHandler) {
|
|
2453
|
+
process.stdout.off("resize", this.resizeHandler);
|
|
2454
|
+
this.resizeHandler = null;
|
|
2455
|
+
}
|
|
2456
|
+
if (process.stdout.isTTY) {
|
|
2457
|
+
process.stdout.write(TERM_RESET_SEQ);
|
|
2458
|
+
}
|
|
2459
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
2460
|
+
process.stdin.setRawMode(this.wasRaw);
|
|
2461
|
+
logger.debug({ wasRaw: this.wasRaw }, "stdin \u6062\u590D");
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
/** 暂停 PC 端 resize 同步——客户端连入时调用 */
|
|
2465
|
+
pauseResize() {
|
|
2466
|
+
if (!this.resizePaused) {
|
|
2467
|
+
this.resizePaused = true;
|
|
2468
|
+
logger.debug("PC \u7AEF resize \u6682\u505C\uFF08\u8FDC\u7A0B\u5BA2\u6237\u7AEF\u63A5\u7BA1\uFF09");
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
/** 恢复 PC 端 resize 同步——所有客户端断开时调用 */
|
|
2472
|
+
resumeResize() {
|
|
2473
|
+
if (this.resizePaused) {
|
|
2474
|
+
this.resizePaused = false;
|
|
2475
|
+
logger.debug("PC \u7AEF resize \u6062\u590D");
|
|
2476
|
+
this.handleResize();
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
// ──────────────── 内部 ────────────────
|
|
2480
|
+
/**
|
|
2481
|
+
* 处理 stdin 输入
|
|
2482
|
+
*
|
|
2483
|
+
* 判定流程:
|
|
2484
|
+
* 1. 是单字节 \x03 或匹配 Kitty CSI u Ctrl+C 序列 → 进入双击检测
|
|
2485
|
+
* - 双击窗口内:触发 onExitRequest(如果设置),不再写 PTY
|
|
2486
|
+
* - 第一次:透传 + 记时间戳
|
|
2487
|
+
* 2. 其它:直接透传
|
|
2488
|
+
*/
|
|
2489
|
+
handleStdin(chunk) {
|
|
2490
|
+
const str = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
2491
|
+
if (this.isCtrlC(str)) {
|
|
2492
|
+
const now = Date.now();
|
|
2493
|
+
if (this.lastCtrlCAt > 0 && now - this.lastCtrlCAt <= DOUBLE_CTRL_C_WINDOW_MS) {
|
|
2494
|
+
this.lastCtrlCAt = 0;
|
|
2495
|
+
if (this.opts.onExitRequest) {
|
|
2496
|
+
logger.info("\u68C0\u6D4B\u5230\u53CC Ctrl+C\uFF0C\u8BF7\u6C42\u9000\u51FA\u4EE3\u7406");
|
|
2497
|
+
this.opts.onExitRequest();
|
|
2498
|
+
return;
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
this.lastCtrlCAt = now;
|
|
2502
|
+
this.pty.write(str);
|
|
2503
|
+
return;
|
|
2504
|
+
}
|
|
2505
|
+
this.pty.write(str);
|
|
2506
|
+
}
|
|
2507
|
+
isCtrlC(s) {
|
|
2508
|
+
if (s.length === 1 && s.charCodeAt(0) === CTRL_C_BYTE)
|
|
2509
|
+
return true;
|
|
2510
|
+
return KITTY_CTRL_C_RE.test(s);
|
|
2511
|
+
}
|
|
2512
|
+
handleResize() {
|
|
2513
|
+
if (this.resizePaused)
|
|
2514
|
+
return;
|
|
2515
|
+
const cols = process.stdout.columns;
|
|
2516
|
+
const rows = process.stdout.rows;
|
|
2517
|
+
if (typeof cols === "number" && typeof rows === "number") {
|
|
2518
|
+
this.pty.resize(cols, rows);
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
};
|
|
2522
|
+
}
|
|
2523
|
+
});
|
|
2524
|
+
|
|
2525
|
+
// backend/dist/auth/token-generator.js
|
|
2526
|
+
import { randomBytes } from "node:crypto";
|
|
2527
|
+
function generateToken() {
|
|
2528
|
+
return randomBytes(TOKEN_BYTES).toString("hex");
|
|
2529
|
+
}
|
|
2530
|
+
function generateSessionId() {
|
|
2531
|
+
return randomBytes(SESSION_ID_BYTES).toString("hex");
|
|
2532
|
+
}
|
|
2533
|
+
var init_token_generator = __esm({
|
|
2534
|
+
"backend/dist/auth/token-generator.js"() {
|
|
2535
|
+
"use strict";
|
|
2536
|
+
init_dist();
|
|
2537
|
+
}
|
|
2538
|
+
});
|
|
2539
|
+
|
|
2540
|
+
// backend/dist/auth/rate-limiter.js
|
|
2541
|
+
var RateLimiter;
|
|
2542
|
+
var init_rate_limiter = __esm({
|
|
2543
|
+
"backend/dist/auth/rate-limiter.js"() {
|
|
2544
|
+
"use strict";
|
|
2545
|
+
init_logger();
|
|
2546
|
+
RateLimiter = class {
|
|
2547
|
+
entries = /* @__PURE__ */ new Map();
|
|
2548
|
+
maxAttempts;
|
|
2549
|
+
windowMs;
|
|
2550
|
+
cleanupTimer = null;
|
|
2551
|
+
/**
|
|
2552
|
+
* @param maxAttempts 窗口内最大允许次数
|
|
2553
|
+
* @param windowMs 窗口长度(毫秒),默认 60s
|
|
2554
|
+
*/
|
|
2555
|
+
constructor(maxAttempts, windowMs = 6e4) {
|
|
2556
|
+
if (!Number.isInteger(maxAttempts) || maxAttempts <= 0) {
|
|
2557
|
+
throw new Error("RateLimiter: maxAttempts \u5FC5\u987B\u662F\u6B63\u6574\u6570");
|
|
2558
|
+
}
|
|
2559
|
+
this.maxAttempts = maxAttempts;
|
|
2560
|
+
this.windowMs = windowMs;
|
|
2561
|
+
this.cleanupTimer = setInterval(() => this.cleanup(), windowMs * 2);
|
|
2562
|
+
if (typeof this.cleanupTimer.unref === "function") {
|
|
2563
|
+
this.cleanupTimer.unref();
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
/**
|
|
2567
|
+
* 尝试一次请求,自动累加计数
|
|
2568
|
+
*
|
|
2569
|
+
* @returns true 通过 / false 已超限被拒
|
|
2570
|
+
*/
|
|
2571
|
+
attempt(ip) {
|
|
2572
|
+
const now = Date.now();
|
|
2573
|
+
const entry = this.entries.get(ip);
|
|
2574
|
+
if (!entry || now >= entry.resetAt) {
|
|
2575
|
+
this.entries.set(ip, { count: 1, resetAt: now + this.windowMs });
|
|
2576
|
+
return true;
|
|
2577
|
+
}
|
|
2578
|
+
entry.count++;
|
|
2579
|
+
if (entry.count > this.maxAttempts) {
|
|
2580
|
+
logger.warn({ ip, count: entry.count, max: this.maxAttempts }, "\u8BA4\u8BC1\u901F\u7387\u8D85\u9650");
|
|
2581
|
+
return false;
|
|
2582
|
+
}
|
|
2583
|
+
return true;
|
|
2584
|
+
}
|
|
2585
|
+
/** 当前窗口内剩余次数 */
|
|
2586
|
+
remaining(ip) {
|
|
2587
|
+
const now = Date.now();
|
|
2588
|
+
const entry = this.entries.get(ip);
|
|
2589
|
+
if (!entry || now >= entry.resetAt)
|
|
2590
|
+
return this.maxAttempts;
|
|
2591
|
+
return Math.max(0, this.maxAttempts - entry.count);
|
|
2592
|
+
}
|
|
2593
|
+
/**
|
|
2594
|
+
* 重置某 IP 的计数(认证成功后清零)
|
|
2595
|
+
*
|
|
2596
|
+
* 让合法用户不会因为之前误输导致后续尝试被限流
|
|
2597
|
+
*/
|
|
2598
|
+
reset(ip) {
|
|
2599
|
+
this.entries.delete(ip);
|
|
2600
|
+
}
|
|
2601
|
+
/** 清理过期 entry(避免内存膨胀) */
|
|
2602
|
+
cleanup() {
|
|
2603
|
+
const now = Date.now();
|
|
2604
|
+
for (const [ip, entry] of this.entries) {
|
|
2605
|
+
if (now >= entry.resetAt) {
|
|
2606
|
+
this.entries.delete(ip);
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
/** 销毁定时器 */
|
|
2611
|
+
destroy() {
|
|
2612
|
+
if (this.cleanupTimer) {
|
|
2613
|
+
clearInterval(this.cleanupTimer);
|
|
2614
|
+
this.cleanupTimer = null;
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
};
|
|
2618
|
+
}
|
|
2619
|
+
});
|
|
2620
|
+
|
|
2621
|
+
// backend/dist/auth/auth-middleware.js
|
|
2622
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2623
|
+
import * as cookie from "cookie";
|
|
2624
|
+
function createSessionCookieName(port) {
|
|
2625
|
+
return `session_id_p${port}`;
|
|
2626
|
+
}
|
|
2627
|
+
var AuthModule;
|
|
2628
|
+
var init_auth_middleware = __esm({
|
|
2629
|
+
"backend/dist/auth/auth-middleware.js"() {
|
|
2630
|
+
"use strict";
|
|
2631
|
+
init_dist();
|
|
2632
|
+
init_token_generator();
|
|
2633
|
+
init_rate_limiter();
|
|
2634
|
+
init_logger();
|
|
2635
|
+
init_errors2();
|
|
2636
|
+
AuthModule = class {
|
|
2637
|
+
token;
|
|
2638
|
+
sessionTtlMs;
|
|
2639
|
+
cookieName;
|
|
2640
|
+
sessions = /* @__PURE__ */ new Map();
|
|
2641
|
+
rateLimiter;
|
|
2642
|
+
constructor(opts) {
|
|
2643
|
+
this.token = opts.token;
|
|
2644
|
+
this.sessionTtlMs = opts.sessionTtlMs;
|
|
2645
|
+
this.cookieName = opts.cookieName;
|
|
2646
|
+
this.rateLimiter = new RateLimiter(opts.rateLimitPerMinute);
|
|
2647
|
+
}
|
|
2648
|
+
// ──────────────── 公共 API ────────────────
|
|
2649
|
+
/**
|
|
2650
|
+
* 时序安全的 token 比对
|
|
2651
|
+
*
|
|
2652
|
+
* 先比长度(不同直接 false);相同则用 timingSafeEqual 恒定时间比较,
|
|
2653
|
+
* 防止攻击者通过响应耗时差异推断 token 字符
|
|
2654
|
+
*/
|
|
2655
|
+
verifyToken(candidate) {
|
|
2656
|
+
const a = Buffer.from(this.token, "utf-8");
|
|
2657
|
+
const b = Buffer.from(candidate, "utf-8");
|
|
2658
|
+
if (a.length !== b.length)
|
|
2659
|
+
return false;
|
|
2660
|
+
return timingSafeEqual(a, b);
|
|
2661
|
+
}
|
|
2662
|
+
/** 创建 session 并返回 sessionId */
|
|
2663
|
+
createSession(ip) {
|
|
2664
|
+
const sid = generateSessionId();
|
|
2665
|
+
this.sessions.set(sid, { createdAt: Date.now(), ip });
|
|
2666
|
+
logger.info({ ip }, "\u5DF2\u521B\u5EFA session");
|
|
2667
|
+
return sid;
|
|
2668
|
+
}
|
|
2669
|
+
/**
|
|
2670
|
+
* 校验 session 是否有效(且未过期)
|
|
2671
|
+
*
|
|
2672
|
+
* 过期的 session 顺手删除(惰性清理)
|
|
2673
|
+
*/
|
|
2674
|
+
validateSession(sessionId) {
|
|
2675
|
+
const entry = this.sessions.get(sessionId);
|
|
2676
|
+
if (!entry)
|
|
2677
|
+
return false;
|
|
2678
|
+
if (Date.now() - entry.createdAt > this.sessionTtlMs) {
|
|
2679
|
+
this.sessions.delete(sessionId);
|
|
2680
|
+
return false;
|
|
2681
|
+
}
|
|
2682
|
+
return true;
|
|
2683
|
+
}
|
|
2684
|
+
/** 从 Express Request 取 sessionId */
|
|
2685
|
+
getSessionFromRequest(req) {
|
|
2686
|
+
const cookies = cookie.parse(req.headers.cookie ?? "");
|
|
2687
|
+
return cookies[this.cookieName] ?? null;
|
|
2688
|
+
}
|
|
2689
|
+
/** 从原始 cookie header 字符串取 sessionId(WS upgrade 用) */
|
|
2690
|
+
getSessionFromCookieHeader(cookieHeader) {
|
|
2691
|
+
const cookies = cookie.parse(cookieHeader);
|
|
2692
|
+
return cookies[this.cookieName] ?? null;
|
|
2693
|
+
}
|
|
2694
|
+
/** 当前 cookie 名(供日志诊断) */
|
|
2695
|
+
getCookieName() {
|
|
2696
|
+
return this.cookieName;
|
|
2697
|
+
}
|
|
2698
|
+
/**
|
|
2699
|
+
* Express 中间件:要求请求带有效 Session
|
|
2700
|
+
*
|
|
2701
|
+
* 失败返回 401 JSON
|
|
2702
|
+
*/
|
|
2703
|
+
requireAuth = (req, res, next) => {
|
|
2704
|
+
const sid = this.getSessionFromRequest(req);
|
|
2705
|
+
if (!sid || !this.validateSession(sid)) {
|
|
2706
|
+
logger.debug({ path: req.path, hasCookie: Boolean(sid) }, "\u8BA4\u8BC1\u5931\u8D25\uFF1A\u65E0\u6709\u6548 session");
|
|
2707
|
+
const err = new AuthError(sid ? ErrorCode.AUTH_SESSION_EXPIRED : ErrorCode.AUTH_TOKEN_MISSING, sid ? "Session \u5DF2\u8FC7\u671F" : "\u672A\u643A\u5E26\u6709\u6548\u51ED\u8BC1", 401);
|
|
2708
|
+
res.status(err.httpStatus).json({ error: err.toPayload() });
|
|
2709
|
+
return;
|
|
2710
|
+
}
|
|
2711
|
+
next();
|
|
2712
|
+
};
|
|
2713
|
+
/**
|
|
2714
|
+
* /api/auth 处理函数
|
|
2715
|
+
*
|
|
2716
|
+
* 流程:取 IP → 限流 → 校验 token → 创建 session → 重置限流 → Set-Cookie
|
|
2717
|
+
*/
|
|
2718
|
+
handleAuth = (req, res) => {
|
|
2719
|
+
const ip = req.ip ?? req.socket.remoteAddress ?? "unknown";
|
|
2720
|
+
if (!this.rateLimiter.attempt(ip)) {
|
|
2721
|
+
const err = new AuthError(ErrorCode.AUTH_RATE_LIMITED, "\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5", 429);
|
|
2722
|
+
res.status(err.httpStatus).json({ error: err.toPayload() });
|
|
2723
|
+
return;
|
|
2724
|
+
}
|
|
2725
|
+
const candidate = req.body?.token;
|
|
2726
|
+
if (typeof candidate !== "string" || !this.verifyToken(candidate)) {
|
|
2727
|
+
const err = new AuthError(ErrorCode.AUTH_INVALID_TOKEN, "Token \u65E0\u6548", 401);
|
|
2728
|
+
logger.info({ ip }, "\u8BA4\u8BC1\u5931\u8D25\uFF1Atoken \u65E0\u6548");
|
|
2729
|
+
res.status(err.httpStatus).json({ error: err.toPayload() });
|
|
2730
|
+
return;
|
|
2731
|
+
}
|
|
2732
|
+
const sid = this.createSession(ip);
|
|
2733
|
+
this.rateLimiter.reset(ip);
|
|
2734
|
+
res.setHeader("Set-Cookie", cookie.serialize(this.cookieName, sid, {
|
|
2735
|
+
httpOnly: true,
|
|
2736
|
+
secure: req.protocol === "https",
|
|
2737
|
+
sameSite: "lax",
|
|
2738
|
+
path: "/",
|
|
2739
|
+
maxAge: Math.floor(this.sessionTtlMs / 1e3)
|
|
2740
|
+
}));
|
|
2741
|
+
logger.info({ ip, cookieName: this.cookieName }, "\u8BA4\u8BC1\u6210\u529F");
|
|
2742
|
+
res.json({ ok: true });
|
|
2743
|
+
};
|
|
2744
|
+
/** 当前活跃 session 数(监控用) */
|
|
2745
|
+
get sessionCount() {
|
|
2746
|
+
return this.sessions.size;
|
|
2747
|
+
}
|
|
2748
|
+
destroy() {
|
|
2749
|
+
this.rateLimiter.destroy();
|
|
2750
|
+
this.sessions.clear();
|
|
2751
|
+
}
|
|
2752
|
+
};
|
|
2753
|
+
}
|
|
2754
|
+
});
|
|
2755
|
+
|
|
2756
|
+
// backend/dist/auth/ws-authenticate.js
|
|
2757
|
+
function createWsAuthenticate(authModule) {
|
|
2758
|
+
return (req) => {
|
|
2759
|
+
const host = req.headers.host ?? "localhost";
|
|
2760
|
+
let url;
|
|
2761
|
+
try {
|
|
2762
|
+
url = new URL(req.url ?? "/", `http://${host}`);
|
|
2763
|
+
} catch {
|
|
2764
|
+
return null;
|
|
2765
|
+
}
|
|
2766
|
+
const tokenParam = url.searchParams.get("token");
|
|
2767
|
+
if (tokenParam) {
|
|
2768
|
+
if (authModule.verifyToken(tokenParam)) {
|
|
2769
|
+
logger.info({ remoteAddress: req.socket.remoteAddress }, "WS \u901A\u8FC7 URL token \u8BA4\u8BC1\uFF08attach\uFF09");
|
|
2770
|
+
return "attach";
|
|
2771
|
+
}
|
|
2772
|
+
logger.warn({ remoteAddress: req.socket.remoteAddress }, "WS URL token \u65E0\u6548");
|
|
2773
|
+
return null;
|
|
2774
|
+
}
|
|
2775
|
+
const cookieHeader = req.headers.cookie ?? "";
|
|
2776
|
+
const sid = authModule.getSessionFromCookieHeader(cookieHeader);
|
|
2777
|
+
if (sid && authModule.validateSession(sid)) {
|
|
2778
|
+
return "webapp";
|
|
2779
|
+
}
|
|
2780
|
+
logger.warn({
|
|
2781
|
+
remoteAddress: req.socket.remoteAddress,
|
|
2782
|
+
cookieNames: cookieHeader.split(";").map((c) => c.trim().split("=")[0]).filter(Boolean),
|
|
2783
|
+
expectedCookie: authModule.getCookieName()
|
|
2784
|
+
}, "WS \u8BA4\u8BC1\u5931\u8D25\uFF1A\u65E0\u6709\u6548 session \u4E5F\u65E0 token");
|
|
2785
|
+
return null;
|
|
2786
|
+
};
|
|
2787
|
+
}
|
|
2788
|
+
var init_ws_authenticate = __esm({
|
|
2789
|
+
"backend/dist/auth/ws-authenticate.js"() {
|
|
2790
|
+
"use strict";
|
|
2791
|
+
init_logger();
|
|
2792
|
+
}
|
|
2793
|
+
});
|
|
2794
|
+
|
|
2795
|
+
// backend/dist/hooks/hook-receiver.js
|
|
2796
|
+
import { EventEmitter as EventEmitter3 } from "node:events";
|
|
2797
|
+
var TOOL_NAME_RE, HookReceiver;
|
|
2798
|
+
var init_hook_receiver = __esm({
|
|
2799
|
+
"backend/dist/hooks/hook-receiver.js"() {
|
|
2800
|
+
"use strict";
|
|
2801
|
+
init_logger();
|
|
2802
|
+
TOOL_NAME_RE = /permission to use (\S+)\s*$/;
|
|
2803
|
+
HookReceiver = class extends EventEmitter3 {
|
|
2804
|
+
/**
|
|
2805
|
+
* 解析一条 hook payload
|
|
2806
|
+
*
|
|
2807
|
+
* @returns 已识别 → notification(同时 emit 'notification' 事件);其他 → ignored
|
|
2808
|
+
*/
|
|
2809
|
+
processHook(payload) {
|
|
2810
|
+
logger.info({ payload }, "\u6536\u5230 hook payload");
|
|
2811
|
+
if (payload.hook_event_name === "PreToolUse") {
|
|
2812
|
+
return { type: "ignored", reason: "pre_tool_use_skipped" };
|
|
2813
|
+
}
|
|
2814
|
+
if (typeof payload.notification_type === "string" && payload.notification_type !== "permission_prompt") {
|
|
2815
|
+
return { type: "ignored", reason: `notification_type=${payload.notification_type}` };
|
|
2816
|
+
}
|
|
2817
|
+
const tool = typeof payload.tool_name === "string" && payload.tool_name || this.extractToolFromMessage(payload.message) || "unknown_tool";
|
|
2818
|
+
const message = typeof payload.message === "string" && payload.message || (typeof payload.tool_name === "string" ? `Tool call: ${payload.tool_name}` : "Approval requested (no details provided)");
|
|
2819
|
+
const notification = { tool, message };
|
|
2820
|
+
logger.info({ tool }, "hook \u8F6C\u6362\u4E3A\u5BA1\u6279\u901A\u77E5");
|
|
2821
|
+
this.emit("notification", notification);
|
|
2822
|
+
return { type: "notification", notification };
|
|
2823
|
+
}
|
|
2824
|
+
/** 从 message 文本提取工具名 */
|
|
2825
|
+
extractToolFromMessage(message) {
|
|
2826
|
+
if (typeof message !== "string")
|
|
2827
|
+
return null;
|
|
2828
|
+
const m = TOOL_NAME_RE.exec(message);
|
|
2829
|
+
return m ? m[1] ?? null : null;
|
|
2830
|
+
}
|
|
2831
|
+
};
|
|
2832
|
+
}
|
|
2833
|
+
});
|
|
2834
|
+
|
|
2835
|
+
// backend/dist/config.js
|
|
2836
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4, copyFileSync } from "node:fs";
|
|
2837
|
+
import { resolve as resolve4, basename as basename2 } from "node:path";
|
|
2838
|
+
import { homedir as homedir2 } from "node:os";
|
|
2839
|
+
function createClaudeSettings(port, existing) {
|
|
2840
|
+
const hookUrl = `http://127.0.0.1:${port}/api/hook`;
|
|
2841
|
+
const hookCommand = `curl -s -X POST ${hookUrl} -H 'Content-Type: application/json' -d @-`;
|
|
2842
|
+
const ourHooks = {
|
|
2843
|
+
Notification: [
|
|
2844
|
+
{
|
|
2845
|
+
matcher: "permission_prompt",
|
|
2846
|
+
hooks: [{ type: "command", command: hookCommand }]
|
|
2847
|
+
}
|
|
2848
|
+
],
|
|
2849
|
+
PreToolUse: [
|
|
2850
|
+
{
|
|
2851
|
+
matcher: "AskUserQuestion",
|
|
2852
|
+
hooks: [{ type: "command", command: hookCommand }]
|
|
2853
|
+
}
|
|
2854
|
+
]
|
|
2855
|
+
};
|
|
2856
|
+
if (!existing) {
|
|
2857
|
+
return { hooks: ourHooks };
|
|
2858
|
+
}
|
|
2859
|
+
const existingHooks = existing["hooks"] && typeof existing["hooks"] === "object" ? existing["hooks"] : {};
|
|
2860
|
+
const overlapped = ["Notification", "PreToolUse"].filter((k) => k in existingHooks);
|
|
2861
|
+
if (overlapped.length > 0) {
|
|
2862
|
+
logger.warn({ overlapped }, "\u7528\u6237\u5DF2\u6709\u540C\u540D hook \u4E8B\u4EF6\u88AB atr \u8986\u76D6\uFF08\u8FD9\u662F\u5FC5\u9700\u7684\uFF09");
|
|
2863
|
+
}
|
|
2864
|
+
return {
|
|
2865
|
+
...existing,
|
|
2866
|
+
hooks: { ...existingHooks, ...ourHooks }
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2869
|
+
function saveClaudeSettings(settings, port, baseDir) {
|
|
2870
|
+
const dir = baseDir ?? resolve4(homedir2(), ATR_DATA_DIR);
|
|
2871
|
+
const settingsDir = resolve4(dir, SETTINGS_DIRNAME);
|
|
2872
|
+
if (!existsSync3(settingsDir)) {
|
|
2873
|
+
mkdirSync4(settingsDir, { recursive: true, mode: 448 });
|
|
2874
|
+
}
|
|
2875
|
+
const path = resolve4(settingsDir, `${port}.json`);
|
|
2876
|
+
writeFileSync3(path, JSON.stringify(settings, null, 2), { encoding: "utf-8", mode: 384 });
|
|
2877
|
+
logger.info({ path, port }, "Claude settings \u5DF2\u5199\u5165");
|
|
2878
|
+
return path;
|
|
2879
|
+
}
|
|
2880
|
+
function shouldInjectSettings(command, envOverride) {
|
|
2881
|
+
if (envOverride !== void 0) {
|
|
2882
|
+
const v = envOverride.trim().toLowerCase();
|
|
2883
|
+
if (v === "true" || v === "1" || v === "yes")
|
|
2884
|
+
return true;
|
|
2885
|
+
if (v === "false" || v === "0" || v === "no")
|
|
2886
|
+
return false;
|
|
2887
|
+
}
|
|
2888
|
+
const base = basename2(command).toLowerCase().replace(/\.(exe|cmd|bat)$/, "");
|
|
2889
|
+
return base === "claude" || base.startsWith("claude-");
|
|
2890
|
+
}
|
|
2891
|
+
function extractSettingsFromArgs(args) {
|
|
2892
|
+
let source = null;
|
|
2893
|
+
let value = null;
|
|
2894
|
+
const remaining = [];
|
|
2895
|
+
for (let i = 0; i < args.length; i++) {
|
|
2896
|
+
const arg = args[i] ?? "";
|
|
2897
|
+
if (arg === "--settings" && i + 1 < args.length) {
|
|
2898
|
+
const v = args[i + 1] ?? "";
|
|
2899
|
+
i++;
|
|
2900
|
+
const parsed = tryParseSettingsValue(v);
|
|
2901
|
+
if (parsed) {
|
|
2902
|
+
source = parsed.source;
|
|
2903
|
+
value = parsed.value;
|
|
2904
|
+
} else {
|
|
2905
|
+
remaining.push(arg, v);
|
|
2906
|
+
}
|
|
2907
|
+
} else if (arg.startsWith("--settings=")) {
|
|
2908
|
+
const v = arg.slice("--settings=".length);
|
|
2909
|
+
const parsed = tryParseSettingsValue(v);
|
|
2910
|
+
if (parsed) {
|
|
2911
|
+
source = parsed.source;
|
|
2912
|
+
value = parsed.value;
|
|
2913
|
+
} else {
|
|
2914
|
+
remaining.push(arg);
|
|
2915
|
+
}
|
|
2916
|
+
} else {
|
|
2917
|
+
remaining.push(arg);
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
if (value === null)
|
|
2921
|
+
return null;
|
|
2922
|
+
return { source: source ?? "inline", value, remainingArgs: remaining };
|
|
2923
|
+
}
|
|
2924
|
+
function tryParseSettingsValue(v) {
|
|
2925
|
+
if (existsSync3(v)) {
|
|
2926
|
+
try {
|
|
2927
|
+
const obj = JSON.parse(readFileSync3(v, "utf-8"));
|
|
2928
|
+
return { source: v, value: obj };
|
|
2929
|
+
} catch (err) {
|
|
2930
|
+
logger.warn({ path: v, err }, "--settings \u6587\u4EF6\u89E3\u6790\u5931\u8D25");
|
|
2931
|
+
return null;
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
try {
|
|
2935
|
+
const obj = JSON.parse(v);
|
|
2936
|
+
return { source: "inline", value: obj };
|
|
2937
|
+
} catch {
|
|
2938
|
+
return null;
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
function defaultUserConfigPath() {
|
|
2942
|
+
return resolve4(homedir2(), ATR_DATA_DIR, CONFIG_FILENAME);
|
|
2943
|
+
}
|
|
2944
|
+
function loadUserConfig(path = defaultUserConfigPath()) {
|
|
2945
|
+
const dir = resolve4(path, "..");
|
|
2946
|
+
if (!existsSync3(dir)) {
|
|
2947
|
+
try {
|
|
2948
|
+
mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
2949
|
+
} catch (err) {
|
|
2950
|
+
logger.warn({ dir, err }, "config \u76EE\u5F55\u521B\u5EFA\u5931\u8D25\uFF08\u7EE7\u7EED\u4EE5\u9ED8\u8BA4\u503C\u8FD0\u884C\uFF09");
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
if (!existsSync3(path)) {
|
|
2954
|
+
const defaults = ensureDefaultUserConfig(null);
|
|
2955
|
+
try {
|
|
2956
|
+
atomicWriteJson(path, defaults);
|
|
2957
|
+
logger.info({ path }, "\u9996\u6B21\u542F\u52A8\uFF1A\u5DF2\u5199\u5165\u9ED8\u8BA4 config.json");
|
|
2958
|
+
} catch (err) {
|
|
2959
|
+
logger.warn({ path, err }, "\u9ED8\u8BA4 config.json \u5199\u5165\u5931\u8D25\uFF08\u4EC5\u5185\u5B58\u4F7F\u7528\u9ED8\u8BA4\u503C\uFF09");
|
|
2960
|
+
}
|
|
2961
|
+
return { path, value: defaults, created: true, recovered: false };
|
|
2962
|
+
}
|
|
2963
|
+
let raw;
|
|
2964
|
+
try {
|
|
2965
|
+
raw = readFileSync3(path, "utf-8");
|
|
2966
|
+
} catch (err) {
|
|
2967
|
+
logger.warn({ path, err }, "config.json \u8BFB\u5931\u8D25\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u503C");
|
|
2968
|
+
return {
|
|
2969
|
+
path,
|
|
2970
|
+
value: ensureDefaultUserConfig(null),
|
|
2971
|
+
created: false,
|
|
2972
|
+
recovered: false
|
|
2973
|
+
};
|
|
2974
|
+
}
|
|
2975
|
+
try {
|
|
2976
|
+
const parsed = JSON.parse(raw);
|
|
2977
|
+
return {
|
|
2978
|
+
path,
|
|
2979
|
+
value: ensureDefaultUserConfig(parsed),
|
|
2980
|
+
created: false,
|
|
2981
|
+
recovered: false
|
|
2982
|
+
};
|
|
2983
|
+
} catch (err) {
|
|
2984
|
+
const backup = `${path}.corrupted-${Date.now()}`;
|
|
2985
|
+
try {
|
|
2986
|
+
copyFileSync(path, backup);
|
|
2987
|
+
logger.warn({ path, backup, err }, "config.json \u89E3\u6790\u5931\u8D25\uFF0C\u5DF2\u5907\u4EFD\u5E76\u843D\u9ED8\u8BA4\u503C");
|
|
2988
|
+
} catch (e2) {
|
|
2989
|
+
logger.error({ path, err, backupErr: e2 }, "config.json \u89E3\u6790\u5931\u8D25\u4E14\u5907\u4EFD\u5931\u8D25");
|
|
2990
|
+
}
|
|
2991
|
+
const defaults = ensureDefaultUserConfig(null);
|
|
2992
|
+
try {
|
|
2993
|
+
atomicWriteJson(path, defaults);
|
|
2994
|
+
} catch (e3) {
|
|
2995
|
+
logger.warn({ path, err: e3 }, "\u9ED8\u8BA4 config.json \u8986\u76D6\u5931\u8D25");
|
|
2996
|
+
}
|
|
2997
|
+
return { path, value: defaults, created: false, recovered: true };
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
function saveUserConfig(value, path = defaultUserConfigPath()) {
|
|
3001
|
+
const dir = resolve4(path, "..");
|
|
3002
|
+
if (!existsSync3(dir)) {
|
|
3003
|
+
mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
3004
|
+
}
|
|
3005
|
+
try {
|
|
3006
|
+
atomicWriteJson(path, value);
|
|
3007
|
+
logger.info({ path }, "config.json \u5DF2\u4FDD\u5B58");
|
|
3008
|
+
} catch (err) {
|
|
3009
|
+
throw new ConfigError(ErrorCode.CONFIG_WRITE_FAILED, `config.json \u5199\u5165\u5931\u8D25\uFF1A${err.message}`, 500, err);
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
function loadConfig(deps) {
|
|
3013
|
+
const { cli, env, generateToken: generateToken2, loadUser = loadUserConfig } = deps;
|
|
3014
|
+
const port = cli.port ?? toInt(env["PORT"]) ?? DEFAULT_PORT;
|
|
3015
|
+
const host = cli.host ?? env["HOST"] ?? "0.0.0.0";
|
|
3016
|
+
const explicitCommand = cli.command ?? env["OCR_COMMAND"] ?? readLegacyEnv(env, "CLAUDE_COMMAND");
|
|
3017
|
+
const claudeCommand = explicitCommand ?? resolveDefaultShell(env);
|
|
3018
|
+
const claudeCwd = cli.workdir ?? env["OCR_CWD"] ?? readLegacyEnv(env, "CLAUDE_CWD") ?? process.cwd();
|
|
3019
|
+
const explicitArgs = mergeClaudeArgs(cli.claudeArgs, env["OCR_ARGS"] ?? readLegacyEnv(env, "CLAUDE_ARGS"));
|
|
3020
|
+
const claudeArgs = explicitArgs.length > 0 || explicitCommand !== void 0 ? explicitArgs : defaultShellArgs(claudeCommand);
|
|
3021
|
+
const instanceName = cli.instanceName ?? env["INSTANCE_NAME"] ?? (basename2(claudeCwd) || "instance");
|
|
3022
|
+
const maxBufferLines = cli.maxBufferLines ?? toInt(env["MAX_BUFFER_LINES"]) ?? DEFAULT_MAX_BUFFER_LINES;
|
|
3023
|
+
const sessionTtlMs = cli.sessionTtlMs ?? toInt(env["SESSION_TTL_MS"]) ?? DEFAULT_SESSION_TTL_MS;
|
|
3024
|
+
const authRateLimit = cli.authRateLimit ?? toInt(env["AUTH_RATE_LIMIT"]) ?? DEFAULT_AUTH_RATE_LIMIT;
|
|
3025
|
+
const noTerminal = cli.noTerminal ?? env["NO_TERMINAL"] === "true";
|
|
3026
|
+
const strictPort = cli.strictPort ?? env["STRICT_PORT"] === "true";
|
|
3027
|
+
const spawnTimeoutSec = cli.spawnTimeoutSec ?? toInt(env["OCR_SPAWN_TIMEOUT"]) ?? DEFAULT_SPAWN_TIMEOUT_SEC;
|
|
3028
|
+
const devProxyPort = cli.devProxy ?? toInt(env["ATR_DEV_PROXY"]);
|
|
3029
|
+
let token;
|
|
3030
|
+
let tokenSource;
|
|
3031
|
+
if (cli.token) {
|
|
3032
|
+
token = cli.token;
|
|
3033
|
+
tokenSource = "cli";
|
|
3034
|
+
} else if (env["AUTH_TOKEN"]) {
|
|
3035
|
+
token = env["AUTH_TOKEN"];
|
|
3036
|
+
tokenSource = "env";
|
|
3037
|
+
} else {
|
|
3038
|
+
token = generateToken2();
|
|
3039
|
+
tokenSource = "generated";
|
|
3040
|
+
}
|
|
3041
|
+
const loaded = loadUser(cli.configPath);
|
|
3042
|
+
return {
|
|
3043
|
+
port,
|
|
3044
|
+
host,
|
|
3045
|
+
token,
|
|
3046
|
+
tokenSource,
|
|
3047
|
+
claudeCommand,
|
|
3048
|
+
claudeArgs,
|
|
3049
|
+
claudeCwd,
|
|
3050
|
+
instanceName,
|
|
3051
|
+
maxBufferLines,
|
|
3052
|
+
sessionTtlMs,
|
|
3053
|
+
authRateLimit,
|
|
3054
|
+
noTerminal,
|
|
3055
|
+
strictPort,
|
|
3056
|
+
spawnTimeoutSec,
|
|
3057
|
+
devProxyPort,
|
|
3058
|
+
userConfig: loaded.value,
|
|
3059
|
+
userConfigPath: loaded.path
|
|
3060
|
+
};
|
|
3061
|
+
}
|
|
3062
|
+
function readLegacyEnv(env, legacyKey) {
|
|
3063
|
+
const v = env[legacyKey];
|
|
3064
|
+
if (v && !warnedLegacy.has(legacyKey)) {
|
|
3065
|
+
warnedLegacy.add(legacyKey);
|
|
3066
|
+
const newKey = legacyKey.replace("CLAUDE_", "OCR_");
|
|
3067
|
+
logger.warn({ legacyKey, newKey }, `\u73AF\u5883\u53D8\u91CF ${legacyKey} \u5DF2\u91CD\u547D\u540D\u4E3A ${newKey}\uFF08\u65E7\u540D\u4ECD\u652F\u6301\uFF0C\u5EFA\u8BAE\u8FC1\u79FB\uFF09`);
|
|
3068
|
+
}
|
|
3069
|
+
return v;
|
|
3070
|
+
}
|
|
3071
|
+
function resolveDefaultShell(env) {
|
|
3072
|
+
const shell = env["SHELL"];
|
|
3073
|
+
if (shell && shell.length > 0)
|
|
3074
|
+
return shell;
|
|
3075
|
+
return process.platform === "win32" ? "cmd.exe" : "/bin/sh";
|
|
3076
|
+
}
|
|
3077
|
+
function defaultShellArgs(command) {
|
|
3078
|
+
const base = command.split("/").pop()?.toLowerCase() ?? "";
|
|
3079
|
+
if (base === "bash" || base === "zsh" || base === "sh") {
|
|
3080
|
+
return ["-i"];
|
|
3081
|
+
}
|
|
3082
|
+
return [];
|
|
3083
|
+
}
|
|
3084
|
+
function mergeClaudeArgs(cliArgs, envJson) {
|
|
3085
|
+
if (cliArgs.length > 0)
|
|
3086
|
+
return cliArgs;
|
|
3087
|
+
if (!envJson)
|
|
3088
|
+
return [];
|
|
3089
|
+
try {
|
|
3090
|
+
const parsed = JSON.parse(envJson);
|
|
3091
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
3092
|
+
} catch {
|
|
3093
|
+
return [];
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
function toInt(s) {
|
|
3097
|
+
if (!s)
|
|
3098
|
+
return void 0;
|
|
3099
|
+
const n = Number(s);
|
|
3100
|
+
return Number.isInteger(n) && n > 0 ? n : void 0;
|
|
3101
|
+
}
|
|
3102
|
+
var warnedLegacy;
|
|
3103
|
+
var init_config = __esm({
|
|
3104
|
+
"backend/dist/config.js"() {
|
|
3105
|
+
"use strict";
|
|
3106
|
+
init_dist();
|
|
3107
|
+
init_errors2();
|
|
3108
|
+
init_logger();
|
|
3109
|
+
init_atomic_write();
|
|
3110
|
+
warnedLegacy = /* @__PURE__ */ new Set();
|
|
3111
|
+
}
|
|
3112
|
+
});
|
|
3113
|
+
|
|
3114
|
+
// backend/dist/registry/shared-token.js
|
|
3115
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync4, mkdirSync as mkdirSync5 } from "node:fs";
|
|
3116
|
+
import { resolve as resolve5 } from "node:path";
|
|
3117
|
+
import { homedir as homedir3 } from "node:os";
|
|
3118
|
+
async function acquireSharedToken(opts) {
|
|
3119
|
+
const path = opts.path ?? defaultPath();
|
|
3120
|
+
const lockDir = opts.lockDir ?? `${defaultDir()}/.shared-token.lock`;
|
|
3121
|
+
const dir = resolve5(path, "..");
|
|
3122
|
+
if (!existsSync4(dir)) {
|
|
3123
|
+
try {
|
|
3124
|
+
mkdirSync5(dir, { recursive: true, mode: 448 });
|
|
3125
|
+
} catch (err) {
|
|
3126
|
+
logger.warn({ dir, err }, "shared-token \u7236\u76EE\u5F55\u521B\u5EFA\u5931\u8D25\uFF08\u7EE7\u7EED\uFF09");
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
return withFileLock(lockDir, () => {
|
|
3130
|
+
const existing = tryReadToken(path);
|
|
3131
|
+
if (existing) {
|
|
3132
|
+
logger.info({ path }, "\u4F7F\u7528\u5DF2\u6709\u7684\u5171\u4EAB token");
|
|
3133
|
+
return { token: existing, source: "shared", path };
|
|
3134
|
+
}
|
|
3135
|
+
const token = opts.generateToken();
|
|
3136
|
+
let cfg;
|
|
3137
|
+
try {
|
|
3138
|
+
cfg = existsSync4(path) ? JSON.parse(readFileSync4(path, "utf-8")) : {};
|
|
3139
|
+
} catch {
|
|
3140
|
+
cfg = {};
|
|
3141
|
+
}
|
|
3142
|
+
cfg.token = token;
|
|
3143
|
+
try {
|
|
3144
|
+
writeFileSync4(path, JSON.stringify(cfg, null, 2), {
|
|
3145
|
+
encoding: "utf-8",
|
|
3146
|
+
mode: 384
|
|
3147
|
+
});
|
|
3148
|
+
logger.info({ path }, "\u751F\u6210\u65B0\u5171\u4EAB token \u5E76\u5199\u76D8");
|
|
3149
|
+
} catch (err) {
|
|
3150
|
+
logger.warn({ path, err }, "token \u5199\u76D8\u5931\u8D25\uFF08\u7EE7\u7EED\u4EE5\u5185\u5B58\u503C\u8FD0\u884C\uFF09");
|
|
3151
|
+
}
|
|
3152
|
+
return { token, source: "generated", path };
|
|
3153
|
+
});
|
|
3154
|
+
}
|
|
3155
|
+
function tryReadToken(path) {
|
|
3156
|
+
if (!existsSync4(path))
|
|
3157
|
+
return null;
|
|
3158
|
+
try {
|
|
3159
|
+
const raw = readFileSync4(path, "utf-8");
|
|
3160
|
+
const obj = JSON.parse(raw);
|
|
3161
|
+
const t = obj.token;
|
|
3162
|
+
if (typeof t !== "string" || t.length === 0)
|
|
3163
|
+
return null;
|
|
3164
|
+
return t;
|
|
3165
|
+
} catch {
|
|
3166
|
+
return null;
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
function defaultDir() {
|
|
3170
|
+
return resolve5(homedir3(), ATR_DATA_DIR);
|
|
3171
|
+
}
|
|
3172
|
+
function defaultPath() {
|
|
3173
|
+
return resolve5(defaultDir(), CONFIG_FILENAME);
|
|
3174
|
+
}
|
|
3175
|
+
var init_shared_token = __esm({
|
|
3176
|
+
"backend/dist/registry/shared-token.js"() {
|
|
3177
|
+
"use strict";
|
|
3178
|
+
init_dist();
|
|
3179
|
+
init_file_lock();
|
|
3180
|
+
init_logger();
|
|
3181
|
+
}
|
|
3182
|
+
});
|
|
3183
|
+
|
|
3184
|
+
// backend/dist/registry/port-finder.js
|
|
3185
|
+
import { createServer } from "node:net";
|
|
3186
|
+
function probePort(port, host) {
|
|
3187
|
+
return new Promise((resolve9) => {
|
|
3188
|
+
const srv = createServer();
|
|
3189
|
+
let settled = false;
|
|
3190
|
+
const done = (ok) => {
|
|
3191
|
+
if (settled)
|
|
3192
|
+
return;
|
|
3193
|
+
settled = true;
|
|
3194
|
+
srv.close(() => resolve9(ok));
|
|
3195
|
+
};
|
|
3196
|
+
srv.once("error", () => done(false));
|
|
3197
|
+
srv.listen(port, host, () => done(true));
|
|
3198
|
+
});
|
|
3199
|
+
}
|
|
3200
|
+
async function bindAvailablePort(opts) {
|
|
3201
|
+
const { preferred, host, server, strict = false } = opts;
|
|
3202
|
+
const maxAttempts = strict ? 1 : opts.maxAttempts ?? PORT_FINDER_MAX_ATTEMPTS;
|
|
3203
|
+
const probe = opts.probe ?? probePort;
|
|
3204
|
+
const listen = opts.listen ?? defaultListen;
|
|
3205
|
+
let lastEaddrPort = null;
|
|
3206
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
3207
|
+
const port = preferred + i;
|
|
3208
|
+
if (port > 65535)
|
|
3209
|
+
break;
|
|
3210
|
+
if (!await probe(port, host)) {
|
|
3211
|
+
lastEaddrPort = port;
|
|
3212
|
+
continue;
|
|
3213
|
+
}
|
|
3214
|
+
try {
|
|
3215
|
+
await listen(server, port, host);
|
|
3216
|
+
} catch (err) {
|
|
3217
|
+
if (isEaddrInUse(err)) {
|
|
3218
|
+
lastEaddrPort = port;
|
|
3219
|
+
continue;
|
|
3220
|
+
}
|
|
3221
|
+
throw err;
|
|
3222
|
+
}
|
|
3223
|
+
if (port !== preferred) {
|
|
3224
|
+
logger.info({ preferred, picked: port }, "\u9996\u9009\u7AEF\u53E3\u88AB\u5360\uFF0C\u5DF2\u9009\u4E0B\u4E00\u4E2A\u53EF\u7528\u7AEF\u53E3");
|
|
3225
|
+
}
|
|
3226
|
+
return { port };
|
|
3227
|
+
}
|
|
3228
|
+
if (strict) {
|
|
3229
|
+
throw new InstanceError(ErrorCode.PORT_UNAVAILABLE, `\u7AEF\u53E3 ${preferred} \u5DF2\u88AB\u5360\u7528\uFF08--strict-port \u542F\u7528\uFF0C\u672A\u5C1D\u8BD5\u81EA\u9002\u5E94\uFF09`, 503);
|
|
3230
|
+
}
|
|
3231
|
+
throw new InstanceError(ErrorCode.PORT_UNAVAILABLE, `\u4ECE\u7AEF\u53E3 ${preferred} \u8D77\u63A2\u6D4B ${maxAttempts} \u4E2A\u5747\u4E0D\u53EF\u7528\uFF08\u6700\u540E EADDRINUSE \u7AEF\u53E3\uFF1A${lastEaddrPort ?? preferred}\uFF09`, 503);
|
|
3232
|
+
}
|
|
3233
|
+
function isEaddrInUse(err) {
|
|
3234
|
+
return typeof err === "object" && err !== null && "code" in err && err.code === "EADDRINUSE";
|
|
3235
|
+
}
|
|
3236
|
+
function defaultListen(server, port, host) {
|
|
3237
|
+
return new Promise((resolve9, reject) => {
|
|
3238
|
+
let settled = false;
|
|
3239
|
+
const onListening = () => {
|
|
3240
|
+
if (settled)
|
|
3241
|
+
return;
|
|
3242
|
+
settled = true;
|
|
3243
|
+
server.removeListener("error", onError);
|
|
3244
|
+
resolve9();
|
|
3245
|
+
};
|
|
3246
|
+
const onError = (err) => {
|
|
3247
|
+
if (settled)
|
|
3248
|
+
return;
|
|
3249
|
+
settled = true;
|
|
3250
|
+
server.removeListener("listening", onListening);
|
|
3251
|
+
reject(err);
|
|
3252
|
+
};
|
|
3253
|
+
server.once("listening", onListening);
|
|
3254
|
+
server.once("error", onError);
|
|
3255
|
+
server.listen(port, host);
|
|
3256
|
+
});
|
|
3257
|
+
}
|
|
3258
|
+
var init_port_finder = __esm({
|
|
3259
|
+
"backend/dist/registry/port-finder.js"() {
|
|
3260
|
+
"use strict";
|
|
3261
|
+
init_dist();
|
|
3262
|
+
init_errors2();
|
|
3263
|
+
init_logger();
|
|
3264
|
+
init_constants2();
|
|
3265
|
+
}
|
|
3266
|
+
});
|
|
3267
|
+
|
|
3268
|
+
// backend/dist/registry/instance-spawner.js
|
|
3269
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
3270
|
+
import { existsSync as existsSync5, statSync as statSync2, openSync } from "node:fs";
|
|
3271
|
+
import { resolve as resolve6, isAbsolute, dirname as dirname3 } from "node:path";
|
|
3272
|
+
function waitForEarlyExit(child, timeoutMs) {
|
|
3273
|
+
return new Promise((res) => {
|
|
3274
|
+
let settled = false;
|
|
3275
|
+
const onExit = (code, signal) => {
|
|
3276
|
+
if (settled)
|
|
3277
|
+
return;
|
|
3278
|
+
settled = true;
|
|
3279
|
+
res({ code, signal });
|
|
3280
|
+
};
|
|
3281
|
+
child.once("exit", onExit);
|
|
3282
|
+
setTimeout(() => {
|
|
3283
|
+
if (settled)
|
|
3284
|
+
return;
|
|
3285
|
+
settled = true;
|
|
3286
|
+
child.off("exit", onExit);
|
|
3287
|
+
res(null);
|
|
3288
|
+
}, timeoutMs);
|
|
3289
|
+
});
|
|
3290
|
+
}
|
|
3291
|
+
function resolveEntry(cliJsPath) {
|
|
3292
|
+
if (existsSync5(cliJsPath)) {
|
|
3293
|
+
return { execPath: process.execPath, args: [cliJsPath] };
|
|
3294
|
+
}
|
|
3295
|
+
const tsPath = cliJsPath.replace(/\.js$/, ".ts");
|
|
3296
|
+
if (existsSync5(tsPath)) {
|
|
3297
|
+
const tsxBin = findTsxBin(dirname3(tsPath));
|
|
3298
|
+
if (tsxBin) {
|
|
3299
|
+
return { execPath: tsxBin, args: [tsPath] };
|
|
3300
|
+
}
|
|
3301
|
+
return {
|
|
3302
|
+
execPath: process.execPath,
|
|
3303
|
+
args: ["--import", "tsx", tsPath]
|
|
3304
|
+
};
|
|
3305
|
+
}
|
|
3306
|
+
throw new InstanceError(ErrorCode.INTERNAL_ERROR, `\u627E\u4E0D\u5230\u5B50\u8FDB\u7A0B\u5165\u53E3\uFF1A${cliJsPath} \u6216 ${tsPath}`, 500);
|
|
3307
|
+
}
|
|
3308
|
+
function findTsxBin(startDir) {
|
|
3309
|
+
let dir = startDir;
|
|
3310
|
+
for (let i = 0; i < 10; i++) {
|
|
3311
|
+
const candidate = resolve6(dir, "node_modules", ".bin", "tsx");
|
|
3312
|
+
if (existsSync5(candidate))
|
|
3313
|
+
return candidate;
|
|
3314
|
+
const parent = dirname3(dir);
|
|
3315
|
+
if (parent === dir)
|
|
3316
|
+
break;
|
|
3317
|
+
dir = parent;
|
|
3318
|
+
}
|
|
3319
|
+
return null;
|
|
3320
|
+
}
|
|
3321
|
+
function basename3(p) {
|
|
3322
|
+
const parts = p.split(/[\\/]/).filter(Boolean);
|
|
3323
|
+
return parts[parts.length - 1] ?? "instance";
|
|
3324
|
+
}
|
|
3325
|
+
var DefaultInstanceSpawner;
|
|
3326
|
+
var init_instance_spawner = __esm({
|
|
3327
|
+
"backend/dist/registry/instance-spawner.js"() {
|
|
3328
|
+
"use strict";
|
|
3329
|
+
init_dist();
|
|
3330
|
+
init_errors2();
|
|
3331
|
+
init_logger();
|
|
3332
|
+
DefaultInstanceSpawner = class {
|
|
3333
|
+
opts;
|
|
3334
|
+
constructor(opts) {
|
|
3335
|
+
this.opts = opts;
|
|
3336
|
+
}
|
|
3337
|
+
async spawn(input) {
|
|
3338
|
+
const cwd = isAbsolute(input.cwd) ? input.cwd : resolve6(input.cwd);
|
|
3339
|
+
if (!existsSync5(cwd)) {
|
|
3340
|
+
throw new InstanceError(ErrorCode.CWD_NOT_EXIST, `\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u5B58\u5728\uFF1A${cwd}`, 400);
|
|
3341
|
+
}
|
|
3342
|
+
if (!statSync2(cwd).isDirectory()) {
|
|
3343
|
+
throw new InstanceError(ErrorCode.CWD_NOT_EXIST, `cwd \u4E0D\u662F\u76EE\u5F55\uFF1A${cwd}`, 400);
|
|
3344
|
+
}
|
|
3345
|
+
const name = input.name && input.name.trim() ? input.name.trim() : basename3(cwd);
|
|
3346
|
+
const { execPath, args: entryArgs } = resolveEntry(this.opts.cliJsPath);
|
|
3347
|
+
const logFd = process.env.ATR_DEBUG_SPAWN ? openSync(`/tmp/atr-spawn-${Date.now()}.log`, "a") : "ignore";
|
|
3348
|
+
const child = spawn2(execPath, [...entryArgs, "--no-terminal"], {
|
|
3349
|
+
cwd,
|
|
3350
|
+
env: {
|
|
3351
|
+
...process.env,
|
|
3352
|
+
...this.opts.env,
|
|
3353
|
+
INSTANCE_NAME: name
|
|
3354
|
+
// 不重置 HOME,让子进程读同一个 ~/.auvezy/terminal-remote/config.json(共享 token)
|
|
3355
|
+
},
|
|
3356
|
+
detached: true,
|
|
3357
|
+
stdio: ["ignore", logFd, logFd]
|
|
3358
|
+
});
|
|
3359
|
+
child.unref();
|
|
3360
|
+
if (typeof child.pid !== "number") {
|
|
3361
|
+
throw new InstanceError(ErrorCode.INTERNAL_ERROR, "\u5B50\u8FDB\u7A0B spawn \u5931\u8D25\uFF1A\u65E0 pid", 500);
|
|
3362
|
+
}
|
|
3363
|
+
const earlyExit = await waitForEarlyExit(child, 600);
|
|
3364
|
+
if (earlyExit !== null) {
|
|
3365
|
+
throw new InstanceError(ErrorCode.INTERNAL_ERROR, `\u5B50\u8FDB\u7A0B\u77AC\u95F4\u9000\u51FA\uFF08exit=${earlyExit.code} signal=${earlyExit.signal}\uFF09\uFF0C\u5E38\u89C1\u539F\u56E0\uFF1Acli \u5165\u53E3\u7F3A\u5931 / \u7AEF\u53E3\u51B2\u7A81 / node_modules \u4E0D\u5168\u3002\u5F00 OTR_DEBUG_SPAWN=1 \u91CD\u542F\u670D\u52A1\u5668\u770B /tmp/atr-spawn-*.log`, 500);
|
|
3366
|
+
}
|
|
3367
|
+
logger.info({ pid: child.pid, cwd, name, exec: execPath, args: entryArgs }, "\u5DF2\u6D3E\u751F headless \u5B9E\u4F8B");
|
|
3368
|
+
return { pid: child.pid, cwd, name };
|
|
3369
|
+
}
|
|
3370
|
+
};
|
|
3371
|
+
}
|
|
3372
|
+
});
|
|
3373
|
+
|
|
3374
|
+
// backend/dist/utils/qrcode-banner.js
|
|
3375
|
+
import QRCode from "qrcode";
|
|
3376
|
+
async function renderQrCode(url, opts = {}) {
|
|
3377
|
+
if (!url)
|
|
3378
|
+
return "";
|
|
3379
|
+
try {
|
|
3380
|
+
return await QRCode.toString(url, {
|
|
3381
|
+
type: "utf8",
|
|
3382
|
+
errorCorrectionLevel: opts.errorCorrectionLevel ?? "L",
|
|
3383
|
+
// utf8 模式本身就是半字符垂直压缩,体积约 qrcode-terminal small=true 的 1/2。
|
|
3384
|
+
// margin: utf8 渲染器在 margin=1 时有"Invalid array length" bug,避开它;
|
|
3385
|
+
// margin=2 视觉上仍然紧凑,且扫码识别率更高
|
|
3386
|
+
margin: 2
|
|
3387
|
+
});
|
|
3388
|
+
} catch {
|
|
3389
|
+
return "";
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
var init_qrcode_banner = __esm({
|
|
3393
|
+
"backend/dist/utils/qrcode-banner.js"() {
|
|
3394
|
+
"use strict";
|
|
3395
|
+
}
|
|
3396
|
+
});
|
|
3397
|
+
|
|
3398
|
+
// backend/dist/utils/wsl-detect.js
|
|
3399
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
3400
|
+
function isWsl(deps) {
|
|
3401
|
+
if (cached !== void 0 && deps === void 0)
|
|
3402
|
+
return cached;
|
|
3403
|
+
const platform = deps?.platform ?? process.platform;
|
|
3404
|
+
if (platform !== "linux") {
|
|
3405
|
+
if (deps === void 0)
|
|
3406
|
+
cached = false;
|
|
3407
|
+
return false;
|
|
3408
|
+
}
|
|
3409
|
+
let content;
|
|
3410
|
+
try {
|
|
3411
|
+
content = (deps?.readProcVersion ?? defaultReadProcVersion)();
|
|
3412
|
+
} catch {
|
|
3413
|
+
if (deps === void 0)
|
|
3414
|
+
cached = false;
|
|
3415
|
+
return false;
|
|
3416
|
+
}
|
|
3417
|
+
const lower = content.toLowerCase();
|
|
3418
|
+
const result = lower.includes("microsoft") || lower.includes("wsl");
|
|
3419
|
+
if (deps === void 0)
|
|
3420
|
+
cached = result;
|
|
3421
|
+
return result;
|
|
3422
|
+
}
|
|
3423
|
+
function defaultReadProcVersion() {
|
|
3424
|
+
return readFileSync5("/proc/version", "utf-8");
|
|
3425
|
+
}
|
|
3426
|
+
var cached;
|
|
3427
|
+
var init_wsl_detect = __esm({
|
|
3428
|
+
"backend/dist/utils/wsl-detect.js"() {
|
|
3429
|
+
"use strict";
|
|
3430
|
+
}
|
|
3431
|
+
});
|
|
3432
|
+
|
|
3433
|
+
// backend/dist/utils/wsl-port-hint.js
|
|
3434
|
+
function isWslNatIp(ip) {
|
|
3435
|
+
const parts = ip.split(".");
|
|
3436
|
+
if (parts.length !== 4)
|
|
3437
|
+
return false;
|
|
3438
|
+
const a = Number(parts[0]);
|
|
3439
|
+
const b = Number(parts[1]);
|
|
3440
|
+
return a === 172 && b >= 16 && b <= 31;
|
|
3441
|
+
}
|
|
3442
|
+
function buildPortForwardHint(ports, wslIp) {
|
|
3443
|
+
const setup = [
|
|
3444
|
+
`$wsl_ip = "${wslIp}"`,
|
|
3445
|
+
...ports.map((p) => `netsh interface portproxy add v4tov4 listenport=${p} listenaddress=0.0.0.0 connectport=${p} connectaddress=$wsl_ip`)
|
|
3446
|
+
];
|
|
3447
|
+
return {
|
|
3448
|
+
title: `\u68C0\u6D4B\u5230 WSL2 NAT \u7F51\u7EDC\u6A21\u5F0F\uFF08${wslIp}\uFF09\u3002Windows \u6D4F\u89C8\u5668\u82E5\u7528 localhost \u8FDE\u4E0D\u4E0A\uFF0C\u8BF7\u5728\u3010\u7BA1\u7406\u5458 PowerShell\u3011\u91CC\u7C98\u8D34\u6267\u884C\u4EE5\u4E0B\u547D\u4EE4\u914D\u7F6E\u7AEF\u53E3\u8F6C\u53D1\uFF1A`,
|
|
3449
|
+
setupCommands: setup,
|
|
3450
|
+
resetCommand: "netsh interface portproxy reset",
|
|
3451
|
+
footer: "WSL \u91CD\u542F\u540E IP \u53EF\u80FD\u53D8\u5316\uFF0C\u9700\u8981\u91CD\u65B0\u6267\u884C\uFF08\u6216\u8FD0\u884C scripts/wsl-port-forward.ps1 \u81EA\u52A8\u5316\uFF09\u3002\u6E05\u7406\uFF1Anetsh interface portproxy reset"
|
|
3452
|
+
};
|
|
3453
|
+
}
|
|
3454
|
+
var init_wsl_port_hint = __esm({
|
|
3455
|
+
"backend/dist/utils/wsl-port-hint.js"() {
|
|
3456
|
+
"use strict";
|
|
3457
|
+
}
|
|
3458
|
+
});
|
|
3459
|
+
|
|
3460
|
+
// backend/dist/utils/ip-monitor.js
|
|
3461
|
+
var IpMonitor;
|
|
3462
|
+
var init_ip_monitor = __esm({
|
|
3463
|
+
"backend/dist/utils/ip-monitor.js"() {
|
|
3464
|
+
"use strict";
|
|
3465
|
+
init_network();
|
|
3466
|
+
init_logger();
|
|
3467
|
+
init_constants2();
|
|
3468
|
+
IpMonitor = class {
|
|
3469
|
+
currentIp;
|
|
3470
|
+
candidateIp = null;
|
|
3471
|
+
candidateCount = 0;
|
|
3472
|
+
timer = null;
|
|
3473
|
+
listener = null;
|
|
3474
|
+
intervalMs;
|
|
3475
|
+
stability;
|
|
3476
|
+
hostHint;
|
|
3477
|
+
detect;
|
|
3478
|
+
constructor(opts) {
|
|
3479
|
+
this.currentIp = opts.initialIp;
|
|
3480
|
+
this.intervalMs = opts.intervalMs ?? IP_MONITOR_INTERVAL_MS;
|
|
3481
|
+
this.stability = opts.stabilityThreshold ?? IP_MONITOR_STABILITY_THRESHOLD;
|
|
3482
|
+
this.hostHint = opts.hostHint;
|
|
3483
|
+
this.detect = opts.detect ?? detectDisplayIp;
|
|
3484
|
+
}
|
|
3485
|
+
/** 注册变更回调(仅一次;重复调用覆盖旧 listener) */
|
|
3486
|
+
onChange(fn) {
|
|
3487
|
+
this.listener = fn;
|
|
3488
|
+
}
|
|
3489
|
+
/** 开始轮询 */
|
|
3490
|
+
start() {
|
|
3491
|
+
if (this.timer)
|
|
3492
|
+
return;
|
|
3493
|
+
this.timer = setInterval(() => this.tick(), this.intervalMs);
|
|
3494
|
+
this.timer.unref();
|
|
3495
|
+
logger.info({ initialIp: this.currentIp, intervalMs: this.intervalMs, stability: this.stability }, "IpMonitor \u542F\u52A8");
|
|
3496
|
+
}
|
|
3497
|
+
/** 停止轮询 */
|
|
3498
|
+
stop() {
|
|
3499
|
+
if (this.timer) {
|
|
3500
|
+
clearInterval(this.timer);
|
|
3501
|
+
this.timer = null;
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
/** 手动 tick:测试用 */
|
|
3505
|
+
tick() {
|
|
3506
|
+
let detected;
|
|
3507
|
+
try {
|
|
3508
|
+
detected = this.detect(this.hostHint);
|
|
3509
|
+
} catch (err) {
|
|
3510
|
+
logger.warn({ err }, "detectDisplayIp \u5931\u8D25");
|
|
3511
|
+
return;
|
|
3512
|
+
}
|
|
3513
|
+
if (detected === this.currentIp) {
|
|
3514
|
+
this.candidateIp = null;
|
|
3515
|
+
this.candidateCount = 0;
|
|
3516
|
+
return;
|
|
3517
|
+
}
|
|
3518
|
+
if (detected !== this.candidateIp) {
|
|
3519
|
+
this.candidateIp = detected;
|
|
3520
|
+
this.candidateCount = 1;
|
|
3521
|
+
return;
|
|
3522
|
+
}
|
|
3523
|
+
this.candidateCount++;
|
|
3524
|
+
if (this.candidateCount >= this.stability) {
|
|
3525
|
+
const oldIp = this.currentIp;
|
|
3526
|
+
this.currentIp = detected;
|
|
3527
|
+
this.candidateIp = null;
|
|
3528
|
+
this.candidateCount = 0;
|
|
3529
|
+
logger.info({ oldIp, newIp: detected }, "displayIp \u5DF2\u53D8\u5316");
|
|
3530
|
+
this.listener?.({ oldIp, newIp: detected });
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
get current() {
|
|
3534
|
+
return this.currentIp;
|
|
3535
|
+
}
|
|
3536
|
+
};
|
|
3537
|
+
}
|
|
3538
|
+
});
|
|
3539
|
+
|
|
3540
|
+
// backend/dist/push/push-service.js
|
|
3541
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, mkdirSync as mkdirSync6 } from "node:fs";
|
|
3542
|
+
import { resolve as resolve7 } from "node:path";
|
|
3543
|
+
import { homedir as homedir4 } from "node:os";
|
|
3544
|
+
import webPush from "web-push";
|
|
3545
|
+
function isLikelySubscription(s) {
|
|
3546
|
+
if (!s || typeof s !== "object")
|
|
3547
|
+
return false;
|
|
3548
|
+
const obj = s;
|
|
3549
|
+
return typeof obj.endpoint === "string" && !!obj.keys && typeof obj.keys === "object" && typeof obj.keys.p256dh === "string" && typeof obj.keys.auth === "string";
|
|
3550
|
+
}
|
|
3551
|
+
var PushService;
|
|
3552
|
+
var init_push_service = __esm({
|
|
3553
|
+
"backend/dist/push/push-service.js"() {
|
|
3554
|
+
"use strict";
|
|
3555
|
+
init_dist();
|
|
3556
|
+
init_errors2();
|
|
3557
|
+
init_logger();
|
|
3558
|
+
init_atomic_write();
|
|
3559
|
+
PushService = class {
|
|
3560
|
+
baseDir;
|
|
3561
|
+
vapidPath;
|
|
3562
|
+
subPath;
|
|
3563
|
+
env;
|
|
3564
|
+
pushImpl;
|
|
3565
|
+
contactEmail;
|
|
3566
|
+
vapid = null;
|
|
3567
|
+
vapidSource = null;
|
|
3568
|
+
subscriptions = [];
|
|
3569
|
+
constructor(opts = {}) {
|
|
3570
|
+
this.baseDir = opts.baseDir ?? resolve7(homedir4(), ATR_DATA_DIR);
|
|
3571
|
+
this.vapidPath = resolve7(this.baseDir, VAPID_KEYS_FILENAME);
|
|
3572
|
+
this.subPath = resolve7(this.baseDir, PUSH_SUBSCRIPTIONS_FILENAME);
|
|
3573
|
+
this.env = opts.env ?? process.env;
|
|
3574
|
+
this.pushImpl = opts.pushImpl ?? webPush;
|
|
3575
|
+
this.contactEmail = opts.contactEmail ?? "mailto:atr@local";
|
|
3576
|
+
}
|
|
3577
|
+
/**
|
|
3578
|
+
* 初始化:读取 / 生成 VAPID + 加载订阅
|
|
3579
|
+
*
|
|
3580
|
+
* 调用方:startServer 启动期一次。
|
|
3581
|
+
*/
|
|
3582
|
+
async init() {
|
|
3583
|
+
this.ensureDir();
|
|
3584
|
+
this.vapid = this.acquireVapid();
|
|
3585
|
+
this.pushImpl.setVapidDetails(this.contactEmail, this.vapid.publicKey, this.vapid.privateKey);
|
|
3586
|
+
this.subscriptions = this.readSubscriptions();
|
|
3587
|
+
logger.info({
|
|
3588
|
+
vapidSource: this.vapidSource,
|
|
3589
|
+
subscriptionCount: this.subscriptions.length
|
|
3590
|
+
}, "PushService \u521D\u59CB\u5316\u5B8C\u6210");
|
|
3591
|
+
}
|
|
3592
|
+
/** 当前 VAPID 公钥(前端订阅用) */
|
|
3593
|
+
getPublicKey() {
|
|
3594
|
+
if (!this.vapid) {
|
|
3595
|
+
throw new PushError(ErrorCode.PUSH_VAPID_NOT_READY, "PushService \u672A\u521D\u59CB\u5316", 503);
|
|
3596
|
+
}
|
|
3597
|
+
return this.vapid.publicKey;
|
|
3598
|
+
}
|
|
3599
|
+
/** 当前订阅数(管理用) */
|
|
3600
|
+
getSubscriptionCount() {
|
|
3601
|
+
return this.subscriptions.length;
|
|
3602
|
+
}
|
|
3603
|
+
/**
|
|
3604
|
+
* 注册订阅(去重:endpoint 相同则覆盖)
|
|
3605
|
+
*
|
|
3606
|
+
* @throws PushError(PUSH_SUBSCRIPTION_INVALID) p256dh 长度异常时
|
|
3607
|
+
*/
|
|
3608
|
+
subscribe(info) {
|
|
3609
|
+
this.validateSubscription(info);
|
|
3610
|
+
const filtered = this.subscriptions.filter((s) => s.endpoint !== info.endpoint);
|
|
3611
|
+
filtered.push(info);
|
|
3612
|
+
this.subscriptions = filtered;
|
|
3613
|
+
this.writeSubscriptions();
|
|
3614
|
+
logger.info({ count: this.subscriptions.length }, "\u8BA2\u9605\u5DF2\u6CE8\u518C");
|
|
3615
|
+
}
|
|
3616
|
+
/** 注销订阅;找不到返回 false */
|
|
3617
|
+
unsubscribe(endpoint) {
|
|
3618
|
+
const before = this.subscriptions.length;
|
|
3619
|
+
this.subscriptions = this.subscriptions.filter((s) => s.endpoint !== endpoint);
|
|
3620
|
+
if (this.subscriptions.length === before)
|
|
3621
|
+
return false;
|
|
3622
|
+
this.writeSubscriptions();
|
|
3623
|
+
logger.info({ count: this.subscriptions.length }, "\u8BA2\u9605\u5DF2\u6CE8\u9500");
|
|
3624
|
+
return true;
|
|
3625
|
+
}
|
|
3626
|
+
/**
|
|
3627
|
+
* 推送给所有订阅者
|
|
3628
|
+
*
|
|
3629
|
+
* 失败 410 Gone → 自动剔除该订阅;其它错误仅 log 不抛
|
|
3630
|
+
*/
|
|
3631
|
+
async notifyAll(payload) {
|
|
3632
|
+
if (!this.vapid)
|
|
3633
|
+
return { sent: 0, pruned: 0, failed: 0 };
|
|
3634
|
+
let sent = 0;
|
|
3635
|
+
let pruned = 0;
|
|
3636
|
+
let failed = 0;
|
|
3637
|
+
const stale = [];
|
|
3638
|
+
for (const sub of this.subscriptions) {
|
|
3639
|
+
try {
|
|
3640
|
+
await this.pushImpl.sendNotification(sub, JSON.stringify(payload));
|
|
3641
|
+
sent++;
|
|
3642
|
+
} catch (err) {
|
|
3643
|
+
const status = err.statusCode;
|
|
3644
|
+
if (status === 404 || status === 410) {
|
|
3645
|
+
stale.push(sub.endpoint);
|
|
3646
|
+
pruned++;
|
|
3647
|
+
} else {
|
|
3648
|
+
failed++;
|
|
3649
|
+
logger.warn({ status, endpoint: sub.endpoint }, "\u63A8\u9001\u5931\u8D25");
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
if (stale.length > 0) {
|
|
3654
|
+
this.subscriptions = this.subscriptions.filter((s) => !stale.includes(s.endpoint));
|
|
3655
|
+
this.writeSubscriptions();
|
|
3656
|
+
}
|
|
3657
|
+
logger.info({ sent, pruned, failed }, "\u63A8\u9001\u6279\u6B21\u5B8C\u6210");
|
|
3658
|
+
return { sent, pruned, failed };
|
|
3659
|
+
}
|
|
3660
|
+
// ───────────── 内部 ─────────────
|
|
3661
|
+
ensureDir() {
|
|
3662
|
+
if (!existsSync6(this.baseDir)) {
|
|
3663
|
+
mkdirSync6(this.baseDir, { recursive: true, mode: 448 });
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
acquireVapid() {
|
|
3667
|
+
const envPub = this.env["VAPID_PUBLIC_KEY"];
|
|
3668
|
+
const envPriv = this.env["VAPID_PRIVATE_KEY"];
|
|
3669
|
+
if (envPub && envPriv) {
|
|
3670
|
+
this.vapidSource = "env";
|
|
3671
|
+
return { publicKey: envPub, privateKey: envPriv };
|
|
3672
|
+
}
|
|
3673
|
+
if (existsSync6(this.vapidPath)) {
|
|
3674
|
+
try {
|
|
3675
|
+
const raw = readFileSync6(this.vapidPath, "utf-8");
|
|
3676
|
+
const parsed = JSON.parse(raw);
|
|
3677
|
+
if (typeof parsed.publicKey === "string" && typeof parsed.privateKey === "string" && parsed.publicKey.length > 0 && parsed.privateKey.length > 0) {
|
|
3678
|
+
this.vapidSource = "file";
|
|
3679
|
+
return parsed;
|
|
3680
|
+
}
|
|
3681
|
+
} catch (err) {
|
|
3682
|
+
logger.warn({ err }, "vapid-keys.json \u89E3\u6790\u5931\u8D25\uFF0C\u5C06\u91CD\u65B0\u751F\u6210");
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
const generated = this.pushImpl.generateVAPIDKeys();
|
|
3686
|
+
atomicWriteJson(this.vapidPath, generated);
|
|
3687
|
+
this.vapidSource = "generated";
|
|
3688
|
+
return generated;
|
|
3689
|
+
}
|
|
3690
|
+
readSubscriptions() {
|
|
3691
|
+
if (!existsSync6(this.subPath))
|
|
3692
|
+
return [];
|
|
3693
|
+
try {
|
|
3694
|
+
const raw = readFileSync6(this.subPath, "utf-8");
|
|
3695
|
+
const parsed = JSON.parse(raw);
|
|
3696
|
+
if (!Array.isArray(parsed))
|
|
3697
|
+
return [];
|
|
3698
|
+
return parsed.filter((s) => isLikelySubscription(s));
|
|
3699
|
+
} catch (err) {
|
|
3700
|
+
logger.warn({ err }, "\u8BA2\u9605\u6587\u4EF6\u89E3\u6790\u5931\u8D25\uFF0C\u91CD\u7F6E\u4E3A\u7A7A");
|
|
3701
|
+
return [];
|
|
3702
|
+
}
|
|
3703
|
+
}
|
|
3704
|
+
writeSubscriptions() {
|
|
3705
|
+
this.ensureDir();
|
|
3706
|
+
atomicWriteJson(this.subPath, this.subscriptions);
|
|
3707
|
+
}
|
|
3708
|
+
/**
|
|
3709
|
+
* 防御性校验:p256dh / auth 长度
|
|
3710
|
+
*
|
|
3711
|
+
* Web Push 的 p256dh 是 P-256 ECDH 公钥(65 字节未压缩),base64url
|
|
3712
|
+
* 编码后约 87 字符。auth 是 16 字节随机串,base64url 约 22 字符。
|
|
3713
|
+
* 异常长度大概率是攻击或客户端 bug,直接拒绝。
|
|
3714
|
+
*/
|
|
3715
|
+
validateSubscription(info) {
|
|
3716
|
+
if (!info.endpoint || typeof info.endpoint !== "string") {
|
|
3717
|
+
throw new PushError(ErrorCode.PUSH_SUBSCRIPTION_INVALID, "\u8BA2\u9605\u7F3A endpoint", 400);
|
|
3718
|
+
}
|
|
3719
|
+
if (!info.keys || typeof info.keys !== "object") {
|
|
3720
|
+
throw new PushError(ErrorCode.PUSH_SUBSCRIPTION_INVALID, "\u8BA2\u9605\u7F3A keys", 400);
|
|
3721
|
+
}
|
|
3722
|
+
const { p256dh, auth } = info.keys;
|
|
3723
|
+
if (typeof p256dh !== "string" || p256dh.length < 80 || p256dh.length > 100) {
|
|
3724
|
+
throw new PushError(ErrorCode.PUSH_SUBSCRIPTION_INVALID, `p256dh \u957F\u5EA6\u5F02\u5E38\uFF1A${p256dh?.length}`, 400);
|
|
3725
|
+
}
|
|
3726
|
+
if (typeof auth !== "string" || auth.length < 16 || auth.length > 32) {
|
|
3727
|
+
throw new PushError(ErrorCode.PUSH_SUBSCRIPTION_INVALID, `auth \u957F\u5EA6\u5F02\u5E38\uFF1A${auth?.length}`, 400);
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
get vapidSourceTag() {
|
|
3731
|
+
return this.vapidSource;
|
|
3732
|
+
}
|
|
3733
|
+
};
|
|
3734
|
+
}
|
|
3735
|
+
});
|
|
3736
|
+
|
|
3737
|
+
// backend/dist/dev/dev-proxy.js
|
|
3738
|
+
import http from "node:http";
|
|
3739
|
+
function createDevProxy(opts) {
|
|
3740
|
+
const targetPort = opts.targetPort;
|
|
3741
|
+
const targetHost = "127.0.0.1";
|
|
3742
|
+
const passthrough = opts.passthroughPrefixes ?? DEFAULT_PASSTHROUGH;
|
|
3743
|
+
const log = opts.logger;
|
|
3744
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
3745
|
+
const trackSocket = (sock) => {
|
|
3746
|
+
if (sockets.has(sock))
|
|
3747
|
+
return;
|
|
3748
|
+
sockets.add(sock);
|
|
3749
|
+
sock.once("close", () => sockets.delete(sock));
|
|
3750
|
+
};
|
|
3751
|
+
const isPassthrough = (path) => passthrough.some((p) => path === p || path.startsWith(p + "/"));
|
|
3752
|
+
const middleware = (req, res, next) => {
|
|
3753
|
+
if (isPassthrough(req.path))
|
|
3754
|
+
return next();
|
|
3755
|
+
const proxyReq = http.request({
|
|
3756
|
+
host: targetHost,
|
|
3757
|
+
port: targetPort,
|
|
3758
|
+
method: req.method,
|
|
3759
|
+
path: req.originalUrl,
|
|
3760
|
+
// 透传 header;删 host 让目标自己重写
|
|
3761
|
+
headers: { ...req.headers, host: `${targetHost}:${targetPort}` }
|
|
3762
|
+
}, (proxyRes) => {
|
|
3763
|
+
res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
|
|
3764
|
+
proxyRes.pipe(res);
|
|
3765
|
+
});
|
|
3766
|
+
proxyReq.on("socket", trackSocket);
|
|
3767
|
+
proxyReq.on("error", (err) => {
|
|
3768
|
+
if (res.headersSent) {
|
|
3769
|
+
res.destroy(err);
|
|
3770
|
+
return;
|
|
3771
|
+
}
|
|
3772
|
+
const code = err.code === "ECONNREFUSED" ? 502 : 500;
|
|
3773
|
+
res.writeHead(code, { "Content-Type": "application/json; charset=utf-8" });
|
|
3774
|
+
res.end(JSON.stringify({
|
|
3775
|
+
ok: false,
|
|
3776
|
+
error: "dev-proxy \u8F6C\u53D1\u5931\u8D25",
|
|
3777
|
+
target: `http://${targetHost}:${targetPort}`,
|
|
3778
|
+
reason: err.code ?? err.message
|
|
3779
|
+
}));
|
|
3780
|
+
log?.warn({ err: err.message, code: err.code }, "dev-proxy HTTP \u8F6C\u53D1\u5931\u8D25");
|
|
3781
|
+
});
|
|
3782
|
+
req.pipe(proxyReq);
|
|
3783
|
+
};
|
|
3784
|
+
const upgradeHandler = (req, clientSock, head) => {
|
|
3785
|
+
if (req.url && (req.url === "/ws" || req.url.startsWith("/ws?")))
|
|
3786
|
+
return;
|
|
3787
|
+
trackSocket(clientSock);
|
|
3788
|
+
const proxyReq = http.request({
|
|
3789
|
+
host: targetHost,
|
|
3790
|
+
port: targetPort,
|
|
3791
|
+
method: "GET",
|
|
3792
|
+
path: req.url,
|
|
3793
|
+
headers: { ...req.headers, host: `${targetHost}:${targetPort}` }
|
|
3794
|
+
});
|
|
3795
|
+
proxyReq.on("upgrade", (proxyRes, upstreamSock, upstreamHead) => {
|
|
3796
|
+
trackSocket(upstreamSock);
|
|
3797
|
+
const lines = [`HTTP/1.1 ${proxyRes.statusCode} ${proxyRes.statusMessage}`];
|
|
3798
|
+
for (const [k, v] of Object.entries(proxyRes.headers)) {
|
|
3799
|
+
if (Array.isArray(v)) {
|
|
3800
|
+
for (const item of v)
|
|
3801
|
+
lines.push(`${k}: ${item}`);
|
|
3802
|
+
} else if (v !== void 0) {
|
|
3803
|
+
lines.push(`${k}: ${v}`);
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3806
|
+
clientSock.write(lines.join("\r\n") + "\r\n\r\n");
|
|
3807
|
+
if (upstreamHead.length > 0)
|
|
3808
|
+
clientSock.write(upstreamHead);
|
|
3809
|
+
upstreamSock.pipe(clientSock);
|
|
3810
|
+
clientSock.pipe(upstreamSock);
|
|
3811
|
+
const closeBoth = () => {
|
|
3812
|
+
upstreamSock.destroy();
|
|
3813
|
+
clientSock.destroy();
|
|
3814
|
+
};
|
|
3815
|
+
upstreamSock.on("error", closeBoth);
|
|
3816
|
+
clientSock.on("error", closeBoth);
|
|
3817
|
+
});
|
|
3818
|
+
proxyReq.on("error", (err) => {
|
|
3819
|
+
log?.warn({ err: err.message }, "dev-proxy WS upgrade \u8F6C\u53D1\u5931\u8D25");
|
|
3820
|
+
clientSock.destroy();
|
|
3821
|
+
});
|
|
3822
|
+
proxyReq.end(head);
|
|
3823
|
+
};
|
|
3824
|
+
opts.httpServer.on("upgrade", upgradeHandler);
|
|
3825
|
+
const dispose = () => {
|
|
3826
|
+
opts.httpServer.removeListener("upgrade", upgradeHandler);
|
|
3827
|
+
for (const sock of sockets) {
|
|
3828
|
+
try {
|
|
3829
|
+
sock.destroy();
|
|
3830
|
+
} catch {
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3833
|
+
sockets.clear();
|
|
3834
|
+
log?.warn({ targetPort }, "dev-proxy \u5DF2\u91CA\u653E");
|
|
3835
|
+
};
|
|
3836
|
+
log?.warn({ targetPort }, "dev-proxy \u5DF2\u542F\u7528");
|
|
3837
|
+
return { middleware, dispose };
|
|
3838
|
+
}
|
|
3839
|
+
var DEFAULT_PASSTHROUGH;
|
|
3840
|
+
var init_dev_proxy = __esm({
|
|
3841
|
+
"backend/dist/dev/dev-proxy.js"() {
|
|
3842
|
+
"use strict";
|
|
3843
|
+
DEFAULT_PASSTHROUGH = ["/api", "/ws"];
|
|
3844
|
+
}
|
|
3845
|
+
});
|
|
3846
|
+
|
|
3847
|
+
// backend/dist/index.js
|
|
3848
|
+
var index_exports = {};
|
|
3849
|
+
__export(index_exports, {
|
|
3850
|
+
isFullAltScreenTui: () => isFullAltScreenTui,
|
|
3851
|
+
resolveAnsiFilterEnabled: () => resolveAnsiFilterEnabled,
|
|
3852
|
+
startServer: () => startServer
|
|
3853
|
+
});
|
|
3854
|
+
import { createServer as createServer2 } from "node:http";
|
|
3855
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
3856
|
+
import { execSync } from "node:child_process";
|
|
3857
|
+
import { networkInterfaces as networkInterfaces3 } from "node:os";
|
|
3858
|
+
import { resolve as resolve8, dirname as dirname4 } from "node:path";
|
|
3859
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
3860
|
+
import express from "express";
|
|
3861
|
+
import cors from "cors";
|
|
3862
|
+
import { randomUUID } from "node:crypto";
|
|
3863
|
+
async function startServer(overrides = {}) {
|
|
3864
|
+
const cli = overrides.cli ?? { subcommand: "start", claudeArgs: [] };
|
|
3865
|
+
if (overrides.port !== void 0)
|
|
3866
|
+
cli.port = overrides.port;
|
|
3867
|
+
if (overrides.token !== void 0)
|
|
3868
|
+
cli.token = overrides.token;
|
|
3869
|
+
const cfg = loadConfig({
|
|
3870
|
+
cli,
|
|
3871
|
+
env: process.env,
|
|
3872
|
+
generateToken
|
|
3873
|
+
});
|
|
3874
|
+
if (cfg.tokenSource === "generated") {
|
|
3875
|
+
try {
|
|
3876
|
+
const r = await acquireSharedToken({
|
|
3877
|
+
path: cfg.userConfigPath,
|
|
3878
|
+
generateToken
|
|
3879
|
+
});
|
|
3880
|
+
cfg.token = r.token;
|
|
3881
|
+
cfg.tokenSource = r.source;
|
|
3882
|
+
} catch (err) {
|
|
3883
|
+
logger.warn({ err }, "shared-token \u83B7\u53D6\u5931\u8D25\uFF0C\u56DE\u9000\u5230\u672C\u8FDB\u7A0B\u968F\u673A token");
|
|
3884
|
+
}
|
|
3885
|
+
}
|
|
3886
|
+
const displayIp = detectDisplayIp(cfg.host);
|
|
3887
|
+
const instanceId = randomUUID();
|
|
3888
|
+
const ipMonitor = new IpMonitor({ initialIp: displayIp, hostHint: cfg.host });
|
|
3889
|
+
const pushService = new PushService();
|
|
3890
|
+
await pushService.init();
|
|
3891
|
+
const app = express();
|
|
3892
|
+
app.use(express.json());
|
|
3893
|
+
const httpServer = createServer2(app);
|
|
3894
|
+
let bindResult;
|
|
3895
|
+
try {
|
|
3896
|
+
bindResult = await bindAvailablePort({
|
|
3897
|
+
preferred: cfg.port,
|
|
3898
|
+
host: cfg.host,
|
|
3899
|
+
server: httpServer,
|
|
3900
|
+
strict: cfg.strictPort
|
|
3901
|
+
});
|
|
3902
|
+
} catch (err) {
|
|
3903
|
+
if (err instanceof InstanceError && err.code === ErrorCode.PORT_UNAVAILABLE) {
|
|
3904
|
+
const hint = cfg.strictPort ? "\u63D0\u793A\uFF1A\u6362\u7528 --port <n> \u6216\u53BB\u6389 --strict-port \u542F\u7528\u81EA\u9002\u5E94" : "\u63D0\u793A\uFF1A\u6362\u7528 --port <n> \u6307\u5B9A\u5176\u4ED6\u8D77\u59CB\u7AEF\u53E3";
|
|
3905
|
+
process.stderr.write(`atr: ${err.message}
|
|
3906
|
+
${hint}
|
|
3907
|
+
`);
|
|
3908
|
+
process.exit(1);
|
|
3909
|
+
}
|
|
3910
|
+
throw err;
|
|
3911
|
+
}
|
|
3912
|
+
cfg.port = bindResult.port;
|
|
3913
|
+
const publicUrl = buildPublicUrl(displayIp, cfg.port, cfg.token);
|
|
3914
|
+
logger.info({
|
|
3915
|
+
port: cfg.port,
|
|
3916
|
+
host: cfg.host,
|
|
3917
|
+
displayIp,
|
|
3918
|
+
claudeCommand: cfg.claudeCommand,
|
|
3919
|
+
claudeCwd: cfg.claudeCwd,
|
|
3920
|
+
claudeArgs: cfg.claudeArgs,
|
|
3921
|
+
instanceName: cfg.instanceName,
|
|
3922
|
+
tokenSource: cfg.tokenSource,
|
|
3923
|
+
userConfigPath: cfg.userConfigPath,
|
|
3924
|
+
strictPort: cfg.strictPort
|
|
3925
|
+
}, "\u52A0\u8F7D\u9636\u6BB5 5 \u914D\u7F6E");
|
|
3926
|
+
const extracted = extractSettingsFromArgs(cfg.claudeArgs);
|
|
3927
|
+
const finalClaudeArgs = extracted ? extracted.remainingArgs : [...cfg.claudeArgs];
|
|
3928
|
+
const settings = createClaudeSettings(cfg.port, extracted?.value);
|
|
3929
|
+
const settingsPath = saveClaudeSettings(settings, cfg.port);
|
|
3930
|
+
if (shouldInjectSettings(cfg.claudeCommand, process.env["ATR_INJECT_SETTINGS"])) {
|
|
3931
|
+
finalClaudeArgs.push("--settings", settingsPath);
|
|
3932
|
+
} else {
|
|
3933
|
+
logger.info({ command: cfg.claudeCommand, settingsPath }, "\u68C0\u6D4B\u5230\u975E Claude \u5B50\u8FDB\u7A0B\uFF0C\u8DF3\u8FC7 --settings \u6CE8\u5165\uFF08hook \u8DEF\u7531\u4ECD\u53EF\u7528\uFF0C\u4F46\u5B50\u8FDB\u7A0B\u4E0D\u4F1A\u81EA\u52A8\u8C03\u7528\uFF09");
|
|
3934
|
+
}
|
|
3935
|
+
const cookieName = createSessionCookieName(cfg.port);
|
|
3936
|
+
const authModule = new AuthModule({
|
|
3937
|
+
token: cfg.token,
|
|
3938
|
+
sessionTtlMs: cfg.sessionTtlMs,
|
|
3939
|
+
rateLimitPerMinute: cfg.authRateLimit,
|
|
3940
|
+
cookieName
|
|
3941
|
+
});
|
|
3942
|
+
const hookReceiver = new HookReceiver();
|
|
3943
|
+
let currentUserConfig = cfg.userConfig;
|
|
3944
|
+
const configStore = {
|
|
3945
|
+
get: () => currentUserConfig,
|
|
3946
|
+
set: (value) => {
|
|
3947
|
+
saveUserConfig(value, cfg.userConfigPath);
|
|
3948
|
+
currentUserConfig = value;
|
|
3949
|
+
}
|
|
3950
|
+
};
|
|
3951
|
+
const __dirname2 = dirname4(fileURLToPath2(import.meta.url));
|
|
3952
|
+
const registry = new InstanceRegistryManager();
|
|
3953
|
+
const spawner = new DefaultInstanceSpawner({
|
|
3954
|
+
cliJsPath: resolve8(__dirname2, "cli.js")
|
|
3955
|
+
});
|
|
3956
|
+
startInstanceWatcher(registry.filePath);
|
|
3957
|
+
const localHostnames = collectLocalHostnames();
|
|
3958
|
+
logger.info({ hostnames: Array.from(localHostnames) }, "CORS \u767D\u540D\u5355");
|
|
3959
|
+
app.use(cors({
|
|
3960
|
+
origin: (origin, callback) => {
|
|
3961
|
+
if (!origin) {
|
|
3962
|
+
callback(null, true);
|
|
3963
|
+
return;
|
|
3964
|
+
}
|
|
3965
|
+
try {
|
|
3966
|
+
const url = new URL(origin);
|
|
3967
|
+
if (localHostnames.has(url.hostname)) {
|
|
3968
|
+
callback(null, true);
|
|
3969
|
+
return;
|
|
3970
|
+
}
|
|
3971
|
+
} catch {
|
|
3972
|
+
}
|
|
3973
|
+
callback(new Error("CORS \u62D2\u7EDD\uFF1Aorigin \u4E0D\u5728\u767D\u540D\u5355"));
|
|
3974
|
+
},
|
|
3975
|
+
credentials: true
|
|
3976
|
+
}));
|
|
3977
|
+
app.use("/api", createApiRouter({
|
|
3978
|
+
authModule,
|
|
3979
|
+
hookReceiver,
|
|
3980
|
+
configStore,
|
|
3981
|
+
registry,
|
|
3982
|
+
currentInstanceId: instanceId,
|
|
3983
|
+
spawner,
|
|
3984
|
+
pushService,
|
|
3985
|
+
port: cfg.port,
|
|
3986
|
+
displayIp
|
|
3987
|
+
}));
|
|
3988
|
+
let devProxy = null;
|
|
3989
|
+
if (cfg.devProxyPort !== void 0) {
|
|
3990
|
+
devProxy = createDevProxy({
|
|
3991
|
+
targetPort: cfg.devProxyPort,
|
|
3992
|
+
httpServer,
|
|
3993
|
+
logger
|
|
3994
|
+
});
|
|
3995
|
+
app.use(devProxy.middleware);
|
|
3996
|
+
}
|
|
3997
|
+
const frontendDist = resolve8(__dirname2, "..", "frontend-dist");
|
|
3998
|
+
if (existsSync7(frontendDist)) {
|
|
3999
|
+
app.use(express.static(frontendDist, {
|
|
4000
|
+
setHeaders: (res, filePath) => {
|
|
4001
|
+
if (filePath.endsWith(".webmanifest")) {
|
|
4002
|
+
res.setHeader("Content-Type", "application/manifest+json; charset=utf-8");
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
4005
|
+
}));
|
|
4006
|
+
app.get("*", (req, res, next) => {
|
|
4007
|
+
if (req.path.startsWith("/api") || req.path.startsWith("/ws")) {
|
|
4008
|
+
return next();
|
|
4009
|
+
}
|
|
4010
|
+
res.sendFile(resolve8(frontendDist, "index.html"));
|
|
4011
|
+
});
|
|
4012
|
+
logger.info({ path: frontendDist }, "\u524D\u7AEF\u9759\u6001\u6587\u4EF6\u5DF2\u6302\u8F7D");
|
|
4013
|
+
} else {
|
|
4014
|
+
logger.warn({ expected: frontendDist }, "\u524D\u7AEF dist \u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u9759\u6001\u670D\u52A1");
|
|
4015
|
+
}
|
|
4016
|
+
const ws = new WsServer(httpServer, { authenticate: createWsAuthenticate(authModule) });
|
|
4017
|
+
const pty2 = new PtyManager();
|
|
4018
|
+
const ansiFilter = resolveAnsiFilterEnabled(cfg.claudeCommand, process.env["OCR_ANSI_FILTER"], process.env["OCR_ANSI_FILTER_TUI_NAMES"]);
|
|
4019
|
+
logger.info({ ansiFilter, command: cfg.claudeCommand }, "AnsiFilter \u7B56\u7565");
|
|
4020
|
+
const ctrl = new SessionController(pty2, ws, cfg.maxBufferLines, {
|
|
4021
|
+
writeToProcessStdout: !cfg.noTerminal,
|
|
4022
|
+
ansiFilter
|
|
4023
|
+
});
|
|
4024
|
+
ctrl.setHookReceiver(hookReceiver);
|
|
4025
|
+
ctrl.setPushService(pushService, {
|
|
4026
|
+
instanceName: cfg.instanceName,
|
|
4027
|
+
url: publicUrl
|
|
4028
|
+
});
|
|
4029
|
+
let relay = null;
|
|
4030
|
+
if (!cfg.noTerminal && process.stdin.isTTY) {
|
|
4031
|
+
relay = new TerminalRelay(pty2, {
|
|
4032
|
+
onExitRequest: () => {
|
|
4033
|
+
process.stderr.write("\n[atr] \u68C0\u6D4B\u5230\u53CC Ctrl+C\uFF0C\u6B63\u5728\u9000\u51FA\u4EE3\u7406\u2026\n");
|
|
4034
|
+
shutdown(0);
|
|
4035
|
+
}
|
|
4036
|
+
});
|
|
4037
|
+
}
|
|
4038
|
+
let shuttingDown = false;
|
|
4039
|
+
const shutdown = (exitCode = 0) => {
|
|
4040
|
+
if (shuttingDown)
|
|
4041
|
+
return;
|
|
4042
|
+
shuttingDown = true;
|
|
4043
|
+
logger.info({ exitCode }, "\u5F00\u59CB\u4F18\u96C5\u5173\u95ED");
|
|
4044
|
+
if (relay)
|
|
4045
|
+
relay.stop();
|
|
4046
|
+
pty2.destroy();
|
|
4047
|
+
if (process.stdout.isTTY) {
|
|
4048
|
+
process.stdout.write("\x1B[?1049l\x1B[?1000l\x1B[?1002l\x1B[?1003l\x1B[?1005l\x1B[?1006l\x1B[?1015l\x1B[?2004l\x1B[?1004l\x1B[?25h\x1B[!p\x1B[0m\x1Bc");
|
|
4049
|
+
try {
|
|
4050
|
+
execSync("stty sane 2>/dev/null", { stdio: "ignore" });
|
|
4051
|
+
} catch {
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
ipMonitor.stop();
|
|
4055
|
+
ctrl.destroy();
|
|
4056
|
+
ws.destroy();
|
|
4057
|
+
authModule.destroy();
|
|
4058
|
+
devProxy?.dispose();
|
|
4059
|
+
stopInstanceWatcher();
|
|
4060
|
+
void registry.unregister(instanceId).catch((err) => {
|
|
4061
|
+
logger.warn({ err }, "\u5173\u95ED\u65F6\u6CE8\u9500\u5B9E\u4F8B\u5931\u8D25");
|
|
4062
|
+
});
|
|
4063
|
+
httpServer.close(() => {
|
|
4064
|
+
logger.info("HTTP server \u5DF2\u5173\u95ED");
|
|
4065
|
+
process.exit(exitCode);
|
|
4066
|
+
});
|
|
4067
|
+
setTimeout(() => process.exit(exitCode), SHUTDOWN_FORCE_EXIT_MS).unref();
|
|
4068
|
+
};
|
|
4069
|
+
pty2.on("exit", (exitCode) => {
|
|
4070
|
+
setTimeout(() => shutdown(exitCode), SHUTDOWN_WS_FLUSH_DELAY_MS);
|
|
4071
|
+
});
|
|
4072
|
+
pty2.on("error", (err) => {
|
|
4073
|
+
logger.error({ err }, "PTY \u9519\u8BEF");
|
|
4074
|
+
if (relay)
|
|
4075
|
+
relay.stop();
|
|
4076
|
+
ctrl.setStatus("idle", err.message);
|
|
4077
|
+
});
|
|
4078
|
+
process.on("SIGINT", () => shutdown(0));
|
|
4079
|
+
process.on("SIGTERM", () => shutdown(0));
|
|
4080
|
+
httpServer.on("error", (err) => {
|
|
4081
|
+
logger.error({ err }, "HTTP server \u8FD0\u884C\u671F\u9519\u8BEF");
|
|
4082
|
+
process.exit(1);
|
|
4083
|
+
});
|
|
4084
|
+
void (async () => {
|
|
4085
|
+
await renderBannerAndStart();
|
|
4086
|
+
})();
|
|
4087
|
+
async function renderBannerAndStart() {
|
|
4088
|
+
const tokenPreview = cfg.token.length >= 16 ? `${cfg.token.slice(0, 8)}...${cfg.token.slice(-8)}` : cfg.token;
|
|
4089
|
+
process.stderr.write("\n");
|
|
4090
|
+
process.stderr.write("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\n");
|
|
4091
|
+
process.stderr.write("\u2551 Auvezy Terminal Remote \xB7 \u542F\u52A8 \u2551\n");
|
|
4092
|
+
process.stderr.write("\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563\n");
|
|
4093
|
+
process.stderr.write(`\u2551 \u5B9E\u4F8B: ${cfg.instanceName.padEnd(38)}\u2551
|
|
4094
|
+
`);
|
|
4095
|
+
process.stderr.write(`\u2551 \u76D1\u542C: http://${cfg.host}:${cfg.port}`.padEnd(53) + "\u2551\n");
|
|
4096
|
+
process.stderr.write(`\u2551 \u626B\u7801: http://${displayIp}:${cfg.port}`.padEnd(53) + "\u2551\n");
|
|
4097
|
+
process.stderr.write(`\u2551 Token: ${tokenPreview.padEnd(38)}\u2551
|
|
4098
|
+
`);
|
|
4099
|
+
process.stderr.write(`\u2551 \u6765\u6E90: ${cfg.tokenSource.padEnd(38)}\u2551
|
|
4100
|
+
`);
|
|
4101
|
+
if (cfg.tokenSource === "generated") {
|
|
4102
|
+
process.stderr.write("\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563\n");
|
|
4103
|
+
process.stderr.write("\u2551 \u5B8C\u6574 Token\uFF08\u9996\u6B21\u663E\u793A\uFF0C\u8BF7\u4FDD\u5B58\uFF09: \u2551\n");
|
|
4104
|
+
process.stderr.write(`\u2551 ${cfg.token.slice(0, 48).padEnd(48)}\u2551
|
|
4105
|
+
`);
|
|
4106
|
+
process.stderr.write(`\u2551 ${cfg.token.slice(48).padEnd(48)}\u2551
|
|
4107
|
+
`);
|
|
4108
|
+
}
|
|
4109
|
+
process.stderr.write("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D\n");
|
|
4110
|
+
const allIps = Array.from(localHostnames).filter((h) => h !== "localhost" && h !== "127.0.0.1" && h !== "::1");
|
|
4111
|
+
const lanIp = isPrivateIp(displayIp) ? displayIp : allIps.find(isPrivateIp);
|
|
4112
|
+
const tailscaleIp = allIps.find(isTailscaleIp);
|
|
4113
|
+
if (lanIp) {
|
|
4114
|
+
const lanUrl = buildPublicUrl(lanIp, cfg.port, cfg.token);
|
|
4115
|
+
const qr = await renderQrCode(lanUrl);
|
|
4116
|
+
if (qr) {
|
|
4117
|
+
process.stderr.write(`
|
|
4118
|
+
\u2500\u2500 \u5C40\u57DF\u7F51 LAN\uFF08${lanIp}\uFF09 \u2500\u2500
|
|
4119
|
+
`);
|
|
4120
|
+
process.stderr.write(qr);
|
|
4121
|
+
process.stderr.write(` ${lanUrl}
|
|
4122
|
+
`);
|
|
4123
|
+
}
|
|
4124
|
+
}
|
|
4125
|
+
if (tailscaleIp && tailscaleIp !== lanIp) {
|
|
4126
|
+
const tsUrl = buildPublicUrl(tailscaleIp, cfg.port, cfg.token);
|
|
4127
|
+
const qr = await renderQrCode(tsUrl);
|
|
4128
|
+
if (qr) {
|
|
4129
|
+
process.stderr.write(`
|
|
4130
|
+
\u2500\u2500 Tailscale\uFF08${tailscaleIp}\uFF09 \u2500\u2500
|
|
4131
|
+
`);
|
|
4132
|
+
process.stderr.write(qr);
|
|
4133
|
+
process.stderr.write(` ${tsUrl}
|
|
4134
|
+
`);
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
4137
|
+
if (!lanIp && !tailscaleIp) {
|
|
4138
|
+
const qr = await renderQrCode(publicUrl);
|
|
4139
|
+
if (qr) {
|
|
4140
|
+
process.stderr.write(`
|
|
4141
|
+
\u2500\u2500 \u5165\u53E3\uFF08${displayIp}\uFF09 \u2500\u2500
|
|
4142
|
+
`);
|
|
4143
|
+
process.stderr.write(qr);
|
|
4144
|
+
process.stderr.write(` ${publicUrl}
|
|
4145
|
+
`);
|
|
4146
|
+
}
|
|
4147
|
+
}
|
|
4148
|
+
const otherIps = allIps.filter((ip) => ip !== lanIp && ip !== tailscaleIp);
|
|
4149
|
+
if (otherIps.length > 0) {
|
|
4150
|
+
process.stderr.write("\n \u5176\u5B83\u53EF\u7528\u5165\u53E3\uFF08VPN / \u591A\u7F51\u5361 / IPv6\uFF09\uFF1A\n");
|
|
4151
|
+
for (const ip of otherIps) {
|
|
4152
|
+
const host = ip.includes(":") ? `[${ip}]` : ip;
|
|
4153
|
+
process.stderr.write(` http://${host}:${cfg.port}/?token=${encodeURIComponent(cfg.token)}
|
|
4154
|
+
`);
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
process.stderr.write(" \u53CC Ctrl+C\uFF08500ms \u5185\uFF09\u9000\u51FA\u4EE3\u7406\uFF1B\u5355\u6B21 Ctrl+C \u900F\u4F20\u7ED9 Claude\n");
|
|
4158
|
+
if (isWsl() && isWslNatIp(displayIp)) {
|
|
4159
|
+
const hint = buildPortForwardHint([cfg.port], displayIp);
|
|
4160
|
+
process.stderr.write("\n");
|
|
4161
|
+
process.stderr.write(" \u250C\u2500\u2500 Windows \u5BBF\u4E3B\u8BBF\u95EE\u63D0\u793A \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
4162
|
+
process.stderr.write(` \u2502 ${hint.title}
|
|
4163
|
+
`);
|
|
4164
|
+
process.stderr.write(" \u2502\n");
|
|
4165
|
+
for (const line of hint.setupCommands) {
|
|
4166
|
+
process.stderr.write(` \u2502 ${line}
|
|
4167
|
+
`);
|
|
4168
|
+
}
|
|
4169
|
+
process.stderr.write(" \u2502\n");
|
|
4170
|
+
process.stderr.write(` \u2502 ${hint.footer}
|
|
4171
|
+
`);
|
|
4172
|
+
process.stderr.write(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
4173
|
+
}
|
|
4174
|
+
if (relay) {
|
|
4175
|
+
process.stderr.write("\n");
|
|
4176
|
+
process.stderr.write(` \u2500\u2500\u2500 \u51C6\u5907\u542F\u52A8 PTY \u5B50\u8FDB\u7A0B\uFF08${cfg.claudeCommand}\uFF09 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4177
|
+
`);
|
|
4178
|
+
const isHeadlessHint = cfg.noTerminal || !process.stdin.isTTY;
|
|
4179
|
+
const mustWaitEnterHint = cli.waitConfirm === true && !isHeadlessHint;
|
|
4180
|
+
if (isHeadlessHint) {
|
|
4181
|
+
process.stderr.write(" \uFF08headless \u6A21\u5F0F\uFF1A\u7ACB\u5373\u542F\u52A8\uFF09\n");
|
|
4182
|
+
} else if (mustWaitEnterHint) {
|
|
4183
|
+
process.stderr.write(" \u6309 Enter \u7ACB\u5373\u542F\u52A8 PTY \u5B50\u8FDB\u7A0B\uFF08--wait-confirm \u6A21\u5F0F\uFF09\n");
|
|
4184
|
+
} else {
|
|
4185
|
+
const timeoutHint = cfg.spawnTimeoutSec > 0 ? `\uFF08${cfg.spawnTimeoutSec}s \u5185\u4EFB\u4E00\u89E6\u53D1\uFF1A\u6D4F\u89C8\u5668\u8FDE\u5165 / \u6309 Enter / \u81EA\u52A8\u542F\u52A8\uFF09` : "\uFF08\u65E0\u8D85\u65F6\uFF1A\u6D4F\u89C8\u5668\u8FDE\u5165\u6216\u6309 Enter \u89E6\u53D1\uFF09";
|
|
4186
|
+
process.stderr.write(` \u626B\u7801\u767B\u5F55\u6D4F\u89C8\u5668\u540E\u81EA\u52A8\u542F\u52A8 ${timeoutHint}
|
|
4187
|
+
`);
|
|
4188
|
+
}
|
|
4189
|
+
process.stderr.write("\n");
|
|
4190
|
+
} else {
|
|
4191
|
+
process.stderr.write("\n");
|
|
4192
|
+
}
|
|
4193
|
+
const isHeadless = cfg.noTerminal || !process.stdin.isTTY;
|
|
4194
|
+
const mustWaitEnter = cli.waitConfirm === true && !isHeadless;
|
|
4195
|
+
let ptyStarted = false;
|
|
4196
|
+
const startPty = (reason) => {
|
|
4197
|
+
if (ptyStarted)
|
|
4198
|
+
return;
|
|
4199
|
+
ptyStarted = true;
|
|
4200
|
+
logger.info({ reason }, "PTY spawn \u89E6\u53D1");
|
|
4201
|
+
try {
|
|
4202
|
+
pty2.spawn({
|
|
4203
|
+
command: cfg.claudeCommand,
|
|
4204
|
+
args: finalClaudeArgs,
|
|
4205
|
+
cwd: cfg.claudeCwd
|
|
4206
|
+
});
|
|
4207
|
+
if (relay)
|
|
4208
|
+
relay.start();
|
|
4209
|
+
ctrl.setStatus("running");
|
|
4210
|
+
} catch (err) {
|
|
4211
|
+
logger.error({ err }, "spawn PTY \u5931\u8D25");
|
|
4212
|
+
ctrl.setStatus("idle", err instanceof Error ? err.message : String(err));
|
|
4213
|
+
}
|
|
4214
|
+
};
|
|
4215
|
+
if (isHeadless) {
|
|
4216
|
+
startPty("immediate");
|
|
4217
|
+
} else if (mustWaitEnter) {
|
|
4218
|
+
void waitForUserConfirm().then(() => startPty("enter")).catch((err) => {
|
|
4219
|
+
logger.error({ err }, "\u7B49\u5F85 Enter \u5931\u8D25");
|
|
4220
|
+
shutdown(1);
|
|
4221
|
+
});
|
|
4222
|
+
} else {
|
|
4223
|
+
ws.onConnect((_ws, type) => {
|
|
4224
|
+
if (type === "webapp")
|
|
4225
|
+
startPty("webapp");
|
|
4226
|
+
});
|
|
4227
|
+
void waitForUserConfirm({ silent: true }).then(() => startPty("enter"));
|
|
4228
|
+
if (cfg.spawnTimeoutSec > 0) {
|
|
4229
|
+
setTimeout(() => startPty("timeout"), cfg.spawnTimeoutSec * 1e3).unref();
|
|
4230
|
+
}
|
|
4231
|
+
}
|
|
4232
|
+
void registry.register({
|
|
4233
|
+
instanceId,
|
|
4234
|
+
name: cfg.instanceName,
|
|
4235
|
+
host: displayIp,
|
|
4236
|
+
port: cfg.port,
|
|
4237
|
+
pid: process.pid,
|
|
4238
|
+
cwd: cfg.claudeCwd,
|
|
4239
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4240
|
+
headless: cfg.noTerminal
|
|
4241
|
+
}).catch((err) => logger.warn({ err }, "\u6CE8\u518C\u5B9E\u4F8B\u5931\u8D25"));
|
|
4242
|
+
ipMonitor.onChange(({ oldIp, newIp }) => {
|
|
4243
|
+
const newUrl = buildPublicUrl(newIp, cfg.port, cfg.token);
|
|
4244
|
+
ws.broadcast({ type: "ip_changed", oldIp, newIp, newUrl });
|
|
4245
|
+
});
|
|
4246
|
+
ipMonitor.start();
|
|
4247
|
+
logger.info({
|
|
4248
|
+
port: cfg.port,
|
|
4249
|
+
host: cfg.host,
|
|
4250
|
+
displayIp,
|
|
4251
|
+
instanceId,
|
|
4252
|
+
instanceName: cfg.instanceName,
|
|
4253
|
+
tokenSource: cfg.tokenSource
|
|
4254
|
+
}, "\u670D\u52A1\u5DF2\u542F\u52A8");
|
|
4255
|
+
}
|
|
4256
|
+
}
|
|
4257
|
+
function basenameLower(command) {
|
|
4258
|
+
return (command.split(/[\\/]/).pop() ?? "").toLowerCase().replace(/\.(exe|cmd|bat|ps1|com)$/, "");
|
|
4259
|
+
}
|
|
4260
|
+
function parseNameSet(env) {
|
|
4261
|
+
if (!env)
|
|
4262
|
+
return /* @__PURE__ */ new Set();
|
|
4263
|
+
return new Set(env.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean));
|
|
4264
|
+
}
|
|
4265
|
+
function isFullAltScreenTui(command, extraNames = void 0) {
|
|
4266
|
+
const base = basenameLower(command);
|
|
4267
|
+
if (!base)
|
|
4268
|
+
return false;
|
|
4269
|
+
if (BUILTIN_FULL_ALT_TUIS.has(base))
|
|
4270
|
+
return true;
|
|
4271
|
+
if (base.startsWith("claude-"))
|
|
4272
|
+
return true;
|
|
4273
|
+
if (parseNameSet(extraNames).has(base))
|
|
4274
|
+
return true;
|
|
4275
|
+
return false;
|
|
4276
|
+
}
|
|
4277
|
+
function resolveAnsiFilterEnabled(command, envOverride, extraTuiNames = void 0) {
|
|
4278
|
+
if (envOverride !== void 0) {
|
|
4279
|
+
const v = envOverride.trim().toLowerCase();
|
|
4280
|
+
if (v === "true" || v === "1" || v === "yes") {
|
|
4281
|
+
if (isFullAltScreenTui(command, extraTuiNames))
|
|
4282
|
+
return false;
|
|
4283
|
+
return true;
|
|
4284
|
+
}
|
|
4285
|
+
if (v === "false" || v === "0" || v === "no")
|
|
4286
|
+
return false;
|
|
4287
|
+
}
|
|
4288
|
+
return false;
|
|
4289
|
+
}
|
|
4290
|
+
function collectLocalHostnames() {
|
|
4291
|
+
const set = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
|
|
4292
|
+
const ifaces = networkInterfaces3();
|
|
4293
|
+
for (const list of Object.values(ifaces)) {
|
|
4294
|
+
if (!list)
|
|
4295
|
+
continue;
|
|
4296
|
+
for (const info of list) {
|
|
4297
|
+
if (info.internal)
|
|
4298
|
+
continue;
|
|
4299
|
+
set.add(info.address);
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
const extra = process.env["OCR_CORS_ALLOW"];
|
|
4303
|
+
if (extra) {
|
|
4304
|
+
for (const h of extra.split(",").map((s) => s.trim()).filter(Boolean)) {
|
|
4305
|
+
set.add(h);
|
|
4306
|
+
}
|
|
4307
|
+
}
|
|
4308
|
+
return set;
|
|
4309
|
+
}
|
|
4310
|
+
function waitForUserConfirm(opts = {}) {
|
|
4311
|
+
if (!process.stdin.isTTY)
|
|
4312
|
+
return Promise.resolve();
|
|
4313
|
+
if (!opts.silent) {
|
|
4314
|
+
process.stderr.write(" \u6309 Enter \u542F\u52A8\u5B50\u8FDB\u7A0B\uFF08\u6216 Ctrl+C \u9000\u51FA backend\uFF09...");
|
|
4315
|
+
}
|
|
4316
|
+
return new Promise((resolve9) => {
|
|
4317
|
+
const onData = (chunk) => {
|
|
4318
|
+
if (chunk.length === 0)
|
|
4319
|
+
return;
|
|
4320
|
+
cleanup();
|
|
4321
|
+
if (!opts.silent) {
|
|
4322
|
+
process.stderr.write("\r\x1B[K");
|
|
4323
|
+
}
|
|
4324
|
+
resolve9();
|
|
4325
|
+
};
|
|
4326
|
+
const cleanup = () => {
|
|
4327
|
+
process.stdin.removeListener("data", onData);
|
|
4328
|
+
process.stdin.pause();
|
|
4329
|
+
};
|
|
4330
|
+
process.stdin.resume();
|
|
4331
|
+
process.stdin.on("data", onData);
|
|
4332
|
+
});
|
|
4333
|
+
}
|
|
4334
|
+
var BUILTIN_FULL_ALT_TUIS;
|
|
4335
|
+
var init_index = __esm({
|
|
4336
|
+
"backend/dist/index.js"() {
|
|
4337
|
+
"use strict";
|
|
4338
|
+
init_dist();
|
|
4339
|
+
init_logger();
|
|
4340
|
+
init_router();
|
|
4341
|
+
init_pty_manager();
|
|
4342
|
+
init_ws_server();
|
|
4343
|
+
init_session_controller();
|
|
4344
|
+
init_terminal_relay();
|
|
4345
|
+
init_auth_middleware();
|
|
4346
|
+
init_token_generator();
|
|
4347
|
+
init_ws_authenticate();
|
|
4348
|
+
init_hook_receiver();
|
|
4349
|
+
init_config();
|
|
4350
|
+
init_shared_token();
|
|
4351
|
+
init_port_finder();
|
|
4352
|
+
init_errors2();
|
|
4353
|
+
init_instance_registry();
|
|
4354
|
+
init_instance_spawner();
|
|
4355
|
+
init_instance_events();
|
|
4356
|
+
init_network();
|
|
4357
|
+
init_qrcode_banner();
|
|
4358
|
+
init_wsl_detect();
|
|
4359
|
+
init_wsl_port_hint();
|
|
4360
|
+
init_ip_monitor();
|
|
4361
|
+
init_push_service();
|
|
4362
|
+
init_dev_proxy();
|
|
4363
|
+
init_constants2();
|
|
4364
|
+
BUILTIN_FULL_ALT_TUIS = /* @__PURE__ */ new Set([
|
|
4365
|
+
"claude",
|
|
4366
|
+
"tmux",
|
|
4367
|
+
"screen",
|
|
4368
|
+
"vim",
|
|
4369
|
+
"nvim",
|
|
4370
|
+
"vi",
|
|
4371
|
+
"htop",
|
|
4372
|
+
"btop",
|
|
4373
|
+
"top",
|
|
4374
|
+
"less",
|
|
4375
|
+
"more",
|
|
4376
|
+
"fzf",
|
|
4377
|
+
"lazygit",
|
|
4378
|
+
"lazydocker",
|
|
4379
|
+
"k9s",
|
|
4380
|
+
"ranger",
|
|
4381
|
+
"mc",
|
|
4382
|
+
"tig"
|
|
4383
|
+
]);
|
|
4384
|
+
}
|
|
4385
|
+
});
|
|
4386
|
+
|
|
4387
|
+
// backend/dist/registry/cli-list.js
|
|
4388
|
+
var cli_list_exports = {};
|
|
4389
|
+
__export(cli_list_exports, {
|
|
4390
|
+
listInstancesCli: () => listInstancesCli
|
|
4391
|
+
});
|
|
4392
|
+
async function listInstancesCli() {
|
|
4393
|
+
const registry = new InstanceRegistryManager();
|
|
4394
|
+
const list = await registry.list();
|
|
4395
|
+
if (list.length === 0) {
|
|
4396
|
+
process.stdout.write("\u672A\u53D1\u73B0\u8FD0\u884C\u4E2D\u7684\u5B9E\u4F8B\n");
|
|
4397
|
+
return 0;
|
|
4398
|
+
}
|
|
4399
|
+
const header = ["PORT", "PID", "INSTANCE NAME", "CWD"];
|
|
4400
|
+
const rows = list.map((i) => [
|
|
4401
|
+
String(i.port),
|
|
4402
|
+
String(i.pid),
|
|
4403
|
+
i.name.slice(0, 30),
|
|
4404
|
+
i.cwd
|
|
4405
|
+
]);
|
|
4406
|
+
const widths = header.map((h, idx) => Math.max(h.length, ...rows.map((r) => r[idx].length)));
|
|
4407
|
+
const printRow = (cells) => {
|
|
4408
|
+
const line = cells.map((c, i) => c.padEnd(widths[i])).join(" ");
|
|
4409
|
+
process.stdout.write(line + "\n");
|
|
4410
|
+
};
|
|
4411
|
+
printRow(header);
|
|
4412
|
+
printRow(widths.map((w) => "-".repeat(w)));
|
|
4413
|
+
for (const r of rows)
|
|
4414
|
+
printRow(r);
|
|
4415
|
+
return 0;
|
|
4416
|
+
}
|
|
4417
|
+
var init_cli_list = __esm({
|
|
4418
|
+
"backend/dist/registry/cli-list.js"() {
|
|
4419
|
+
"use strict";
|
|
4420
|
+
init_instance_registry();
|
|
4421
|
+
}
|
|
4422
|
+
});
|
|
4423
|
+
|
|
4424
|
+
// backend/dist/registry/cli-stop.js
|
|
4425
|
+
var cli_stop_exports = {};
|
|
4426
|
+
__export(cli_stop_exports, {
|
|
4427
|
+
stopInstancesCli: () => stopInstancesCli
|
|
4428
|
+
});
|
|
4429
|
+
async function stopInstancesCli(pattern) {
|
|
4430
|
+
try {
|
|
4431
|
+
const results = await stopInstances(pattern);
|
|
4432
|
+
if (results.length === 0) {
|
|
4433
|
+
const hint = pattern ? `\uFF08pattern="${pattern}"\uFF09` : "";
|
|
4434
|
+
process.stdout.write(`\u672A\u5339\u914D\u5230\u4EFB\u4F55\u5B9E\u4F8B ${hint}
|
|
4435
|
+
`);
|
|
4436
|
+
return 1;
|
|
4437
|
+
}
|
|
4438
|
+
for (const r of results) {
|
|
4439
|
+
const tag = r.outcome === "sigterm" ? "\u2713" : r.outcome === "sigkill" ? "\u2717 \u5F3A\u6740" : r.outcome === "gone" ? "\xB7 \u5DF2\u79BB\u7EBF" : "\u2717 \u5931\u8D25";
|
|
4440
|
+
process.stdout.write(`${tag} port=${r.instance.port} pid=${r.instance.pid} name=${r.instance.name}` + (r.error ? ` err=${r.error}` : "") + "\n");
|
|
4441
|
+
}
|
|
4442
|
+
return 0;
|
|
4443
|
+
} catch (err) {
|
|
4444
|
+
process.stderr.write(`[atr] stop \u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
|
|
4445
|
+
`);
|
|
4446
|
+
return 2;
|
|
4447
|
+
}
|
|
4448
|
+
}
|
|
4449
|
+
var init_cli_stop = __esm({
|
|
4450
|
+
"backend/dist/registry/cli-stop.js"() {
|
|
4451
|
+
"use strict";
|
|
4452
|
+
init_stop_instances();
|
|
4453
|
+
}
|
|
4454
|
+
});
|
|
4455
|
+
|
|
4456
|
+
// backend/dist/attach/attach-client.js
|
|
4457
|
+
import WebSocket3 from "ws";
|
|
4458
|
+
import { EventEmitter as EventEmitter4 } from "node:events";
|
|
4459
|
+
function normalizeAttachUrl(input) {
|
|
4460
|
+
let url;
|
|
4461
|
+
try {
|
|
4462
|
+
url = new URL(input);
|
|
4463
|
+
} catch {
|
|
4464
|
+
throw new Error(`\u65E0\u6548 URL\uFF1A${input}`);
|
|
4465
|
+
}
|
|
4466
|
+
if (url.protocol === "http:")
|
|
4467
|
+
url.protocol = "ws:";
|
|
4468
|
+
else if (url.protocol === "https:")
|
|
4469
|
+
url.protocol = "wss:";
|
|
4470
|
+
else if (url.protocol !== "ws:" && url.protocol !== "wss:") {
|
|
4471
|
+
throw new Error(`\u4E0D\u652F\u6301\u7684\u534F\u8BAE\uFF1A${url.protocol}`);
|
|
4472
|
+
}
|
|
4473
|
+
url.pathname = "/ws";
|
|
4474
|
+
return url.toString();
|
|
4475
|
+
}
|
|
4476
|
+
var AttachClient;
|
|
4477
|
+
var init_attach_client = __esm({
|
|
4478
|
+
"backend/dist/attach/attach-client.js"() {
|
|
4479
|
+
"use strict";
|
|
4480
|
+
init_dist();
|
|
4481
|
+
init_logger();
|
|
4482
|
+
init_constants2();
|
|
4483
|
+
AttachClient = class extends EventEmitter4 {
|
|
4484
|
+
url;
|
|
4485
|
+
reconnectDelays;
|
|
4486
|
+
autoReconnect;
|
|
4487
|
+
ws = null;
|
|
4488
|
+
reconnectAttempt = 0;
|
|
4489
|
+
reconnectTimer = null;
|
|
4490
|
+
destroyed = false;
|
|
4491
|
+
_status = "connecting";
|
|
4492
|
+
constructor(opts) {
|
|
4493
|
+
super();
|
|
4494
|
+
this.url = normalizeAttachUrl(opts.url);
|
|
4495
|
+
this.reconnectDelays = opts.reconnectDelaysMs ?? ATTACH_RECONNECT_DELAYS_MS;
|
|
4496
|
+
this.autoReconnect = opts.autoReconnect ?? true;
|
|
4497
|
+
}
|
|
4498
|
+
emit(event, ...args) {
|
|
4499
|
+
return super.emit(event, ...args);
|
|
4500
|
+
}
|
|
4501
|
+
on(event, listener) {
|
|
4502
|
+
return super.on(event, listener);
|
|
4503
|
+
}
|
|
4504
|
+
once(event, listener) {
|
|
4505
|
+
return super.once(event, listener);
|
|
4506
|
+
}
|
|
4507
|
+
/** 连接(非阻塞;调用方监听事件即可) */
|
|
4508
|
+
connect() {
|
|
4509
|
+
if (this.destroyed)
|
|
4510
|
+
return;
|
|
4511
|
+
this.setStatus("connecting");
|
|
4512
|
+
const ws = new WebSocket3(this.url);
|
|
4513
|
+
this.ws = ws;
|
|
4514
|
+
ws.on("open", () => {
|
|
4515
|
+
this.reconnectAttempt = 0;
|
|
4516
|
+
this.setStatus("connected");
|
|
4517
|
+
logger.info({ url: this.url }, "attach WS \u5DF2\u8FDE\u63A5");
|
|
4518
|
+
});
|
|
4519
|
+
ws.on("message", (raw) => {
|
|
4520
|
+
let parsed;
|
|
4521
|
+
try {
|
|
4522
|
+
parsed = JSON.parse(raw.toString());
|
|
4523
|
+
} catch {
|
|
4524
|
+
logger.warn("\u6536\u5230\u975E JSON WS \u6D88\u606F\uFF0C\u5FFD\u7565");
|
|
4525
|
+
return;
|
|
4526
|
+
}
|
|
4527
|
+
if (!isServerMessage(parsed)) {
|
|
4528
|
+
logger.warn("\u6536\u5230\u4E0D\u8BC6\u522B\u7684 server message\uFF0C\u5FFD\u7565");
|
|
4529
|
+
return;
|
|
4530
|
+
}
|
|
4531
|
+
this.handleServerMessage(parsed);
|
|
4532
|
+
});
|
|
4533
|
+
ws.on("close", (code) => {
|
|
4534
|
+
logger.info({ code }, "attach WS \u5173\u95ED");
|
|
4535
|
+
this.ws = null;
|
|
4536
|
+
this.setStatus("disconnected");
|
|
4537
|
+
if (code === 1008 || code === 4401) {
|
|
4538
|
+
this.emit("fatal", "\u8BA4\u8BC1\u5931\u8D25\u6216\u88AB\u670D\u52A1\u7AEF\u62D2\u7EDD");
|
|
4539
|
+
return;
|
|
4540
|
+
}
|
|
4541
|
+
if (this.autoReconnect && !this.destroyed) {
|
|
4542
|
+
this.scheduleReconnect();
|
|
4543
|
+
}
|
|
4544
|
+
});
|
|
4545
|
+
ws.on("error", (err) => {
|
|
4546
|
+
logger.warn({ err: err.message }, "attach WS \u9519\u8BEF");
|
|
4547
|
+
});
|
|
4548
|
+
}
|
|
4549
|
+
/** 写入用户输入(外部 stdin 喂过来) */
|
|
4550
|
+
write(data) {
|
|
4551
|
+
this.send({ type: "user_input", data });
|
|
4552
|
+
}
|
|
4553
|
+
/** PTY 尺寸调整(外部 SIGWINCH 触发) */
|
|
4554
|
+
resize(cols, rows) {
|
|
4555
|
+
this.send({ type: "resize", cols, rows });
|
|
4556
|
+
}
|
|
4557
|
+
/** 关闭连接,停止重连 */
|
|
4558
|
+
destroy() {
|
|
4559
|
+
this.destroyed = true;
|
|
4560
|
+
if (this.reconnectTimer) {
|
|
4561
|
+
clearTimeout(this.reconnectTimer);
|
|
4562
|
+
this.reconnectTimer = null;
|
|
4563
|
+
}
|
|
4564
|
+
if (this.ws) {
|
|
4565
|
+
try {
|
|
4566
|
+
this.ws.close(1e3, "attach destroy");
|
|
4567
|
+
} catch {
|
|
4568
|
+
}
|
|
4569
|
+
this.ws = null;
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4572
|
+
get status() {
|
|
4573
|
+
return this._status;
|
|
4574
|
+
}
|
|
4575
|
+
// ────────────────── 内部 ──────────────────
|
|
4576
|
+
send(msg) {
|
|
4577
|
+
if (!this.ws || this.ws.readyState !== WebSocket3.OPEN)
|
|
4578
|
+
return;
|
|
4579
|
+
try {
|
|
4580
|
+
this.ws.send(JSON.stringify(msg));
|
|
4581
|
+
} catch (err) {
|
|
4582
|
+
logger.warn({ err: err.message }, "WS send \u5931\u8D25");
|
|
4583
|
+
}
|
|
4584
|
+
}
|
|
4585
|
+
setStatus(s) {
|
|
4586
|
+
if (s === this._status)
|
|
4587
|
+
return;
|
|
4588
|
+
this._status = s;
|
|
4589
|
+
this.emit("connectionStatus", s);
|
|
4590
|
+
}
|
|
4591
|
+
handleServerMessage(msg) {
|
|
4592
|
+
switch (msg.type) {
|
|
4593
|
+
case "terminal_output":
|
|
4594
|
+
this.emit("output", msg.data);
|
|
4595
|
+
return;
|
|
4596
|
+
case "history_sync":
|
|
4597
|
+
this.emit("output", msg.data);
|
|
4598
|
+
if (typeof msg.cols === "number" && typeof msg.rows === "number") {
|
|
4599
|
+
this.emit("resize", msg.cols, msg.rows);
|
|
4600
|
+
}
|
|
4601
|
+
this.emit("status", msg.status);
|
|
4602
|
+
return;
|
|
4603
|
+
case "status_update":
|
|
4604
|
+
this.emit("status", msg.status, msg.detail);
|
|
4605
|
+
return;
|
|
4606
|
+
case "terminal_resize":
|
|
4607
|
+
this.emit("resize", msg.cols, msg.rows);
|
|
4608
|
+
return;
|
|
4609
|
+
case "session_ended":
|
|
4610
|
+
this.emit("sessionEnded", msg.exitCode, msg.reason);
|
|
4611
|
+
return;
|
|
4612
|
+
case "error":
|
|
4613
|
+
this.emit("output", `\r
|
|
4614
|
+
\x1B[31m[server error: ${msg.code}: ${msg.message}]\x1B[0m\r
|
|
4615
|
+
`);
|
|
4616
|
+
return;
|
|
4617
|
+
case "heartbeat":
|
|
4618
|
+
case "ip_changed":
|
|
4619
|
+
return;
|
|
4620
|
+
}
|
|
4621
|
+
}
|
|
4622
|
+
scheduleReconnect() {
|
|
4623
|
+
if (this.destroyed)
|
|
4624
|
+
return;
|
|
4625
|
+
const idx = Math.min(this.reconnectAttempt, this.reconnectDelays.length - 1);
|
|
4626
|
+
const delay = this.reconnectDelays[idx] ?? 3e4;
|
|
4627
|
+
this.reconnectAttempt++;
|
|
4628
|
+
logger.info({ attempt: this.reconnectAttempt, delay }, "\u5B89\u6392 attach \u91CD\u8FDE");
|
|
4629
|
+
this.reconnectTimer = setTimeout(() => {
|
|
4630
|
+
this.reconnectTimer = null;
|
|
4631
|
+
this.connect();
|
|
4632
|
+
}, delay);
|
|
4633
|
+
}
|
|
4634
|
+
};
|
|
4635
|
+
}
|
|
4636
|
+
});
|
|
4637
|
+
|
|
4638
|
+
// backend/dist/attach.js
|
|
4639
|
+
var attach_exports = {};
|
|
4640
|
+
__export(attach_exports, {
|
|
4641
|
+
runAttachCli: () => runAttachCli
|
|
4642
|
+
});
|
|
4643
|
+
async function runAttachCli(url) {
|
|
4644
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
4645
|
+
process.stderr.write("[atr] attach \u9700\u8981\u4EA4\u4E92\u5F0F\u7EC8\u7AEF\n");
|
|
4646
|
+
return 2;
|
|
4647
|
+
}
|
|
4648
|
+
const client = new AttachClient({ url });
|
|
4649
|
+
client.on("output", (data) => {
|
|
4650
|
+
process.stdout.write(data);
|
|
4651
|
+
});
|
|
4652
|
+
client.on("resize", (cols, rows) => {
|
|
4653
|
+
process.stderr.write(`\x1B[2K\r[remote resize ${cols}x${rows}]
|
|
4654
|
+
`);
|
|
4655
|
+
});
|
|
4656
|
+
client.on("status", (status, detail) => {
|
|
4657
|
+
process.stderr.write(`\x1B[2K\r[remote status: ${status}${detail ? ` \xB7 ${detail}` : ""}]
|
|
4658
|
+
`);
|
|
4659
|
+
});
|
|
4660
|
+
client.on("connectionStatus", (s) => {
|
|
4661
|
+
if (s === "disconnected") {
|
|
4662
|
+
process.stderr.write("\x1B[2K\r[attach \u5DF2\u65AD\u5F00\uFF0C\u81EA\u52A8\u91CD\u8FDE\u4E2D\u2026]\n");
|
|
4663
|
+
} else if (s === "connected") {
|
|
4664
|
+
process.stderr.write("\x1B[2K\r[attach \u5DF2\u8FDE\u63A5]\n");
|
|
4665
|
+
}
|
|
4666
|
+
});
|
|
4667
|
+
let exitCode = 0;
|
|
4668
|
+
const finish = (code) => {
|
|
4669
|
+
exitCode = code;
|
|
4670
|
+
cleanup();
|
|
4671
|
+
};
|
|
4672
|
+
client.on("sessionEnded", (code, reason) => {
|
|
4673
|
+
process.stderr.write(`\x1B[2K\r[\u8FDC\u7AEF\u4F1A\u8BDD\u7ED3\u675F \xB7 exit ${code} \xB7 ${reason}]
|
|
4674
|
+
`);
|
|
4675
|
+
finish(code);
|
|
4676
|
+
});
|
|
4677
|
+
client.on("fatal", (msg) => {
|
|
4678
|
+
process.stderr.write(`\x1B[31m[attach \u81F4\u547D\u9519\u8BEF] ${msg}\x1B[0m
|
|
4679
|
+
`);
|
|
4680
|
+
finish(1);
|
|
4681
|
+
});
|
|
4682
|
+
process.stdin.setRawMode(true);
|
|
4683
|
+
process.stdin.resume();
|
|
4684
|
+
process.stdin.setEncoding("utf8");
|
|
4685
|
+
let lastCtrlC = 0;
|
|
4686
|
+
const onStdin = (chunk) => {
|
|
4687
|
+
if (chunk === "") {
|
|
4688
|
+
const now = Date.now();
|
|
4689
|
+
if (now - lastCtrlC <= DOUBLE_CTRL_C_WINDOW_MS) {
|
|
4690
|
+
process.stderr.write("\n[atr] \u53CC Ctrl+C\uFF1A\u65AD\u5F00 attach\n");
|
|
4691
|
+
finish(0);
|
|
4692
|
+
return;
|
|
4693
|
+
}
|
|
4694
|
+
lastCtrlC = now;
|
|
4695
|
+
}
|
|
4696
|
+
client.write(chunk);
|
|
4697
|
+
};
|
|
4698
|
+
process.stdin.on("data", onStdin);
|
|
4699
|
+
const reportResize = () => {
|
|
4700
|
+
if (process.stdout.columns && process.stdout.rows) {
|
|
4701
|
+
client.resize(process.stdout.columns, process.stdout.rows);
|
|
4702
|
+
}
|
|
4703
|
+
};
|
|
4704
|
+
process.on("SIGWINCH", reportResize);
|
|
4705
|
+
setTimeout(reportResize, 100);
|
|
4706
|
+
const onSig = () => finish(0);
|
|
4707
|
+
process.on("SIGINT", onSig);
|
|
4708
|
+
process.on("SIGTERM", onSig);
|
|
4709
|
+
let cleanedUp = false;
|
|
4710
|
+
function cleanup() {
|
|
4711
|
+
if (cleanedUp)
|
|
4712
|
+
return;
|
|
4713
|
+
cleanedUp = true;
|
|
4714
|
+
try {
|
|
4715
|
+
process.stdin.setRawMode(false);
|
|
4716
|
+
} catch {
|
|
4717
|
+
}
|
|
4718
|
+
process.stdin.off("data", onStdin);
|
|
4719
|
+
process.stdin.pause();
|
|
4720
|
+
process.off("SIGWINCH", reportResize);
|
|
4721
|
+
process.off("SIGINT", onSig);
|
|
4722
|
+
process.off("SIGTERM", onSig);
|
|
4723
|
+
client.destroy();
|
|
4724
|
+
}
|
|
4725
|
+
client.connect();
|
|
4726
|
+
process.stderr.write(`[attach] \u8FDE\u63A5 ${url}
|
|
4727
|
+
`);
|
|
4728
|
+
return new Promise((resolve9) => {
|
|
4729
|
+
const wait = setInterval(() => {
|
|
4730
|
+
if (cleanedUp) {
|
|
4731
|
+
clearInterval(wait);
|
|
4732
|
+
resolve9(exitCode);
|
|
4733
|
+
}
|
|
4734
|
+
}, 100);
|
|
4735
|
+
});
|
|
4736
|
+
}
|
|
4737
|
+
var init_attach = __esm({
|
|
4738
|
+
"backend/dist/attach.js"() {
|
|
4739
|
+
"use strict";
|
|
4740
|
+
init_attach_client();
|
|
4741
|
+
init_constants2();
|
|
4742
|
+
}
|
|
4743
|
+
});
|
|
4744
|
+
|
|
4745
|
+
// backend/dist/__cli-entry.tmp.js
|
|
4746
|
+
process.env["CLI_MODE"] = "true";
|
|
4747
|
+
void (async () => {
|
|
4748
|
+
const { parseCliArgs: parseCliArgs2, HELP_TEXT: HELP_TEXT2 } = await Promise.resolve().then(() => (init_cli_utils(), cli_utils_exports));
|
|
4749
|
+
const { startServer: startServer2 } = await Promise.resolve().then(() => (init_index(), index_exports));
|
|
4750
|
+
let cli;
|
|
4751
|
+
try {
|
|
4752
|
+
cli = parseCliArgs2(process.argv.slice(2));
|
|
4753
|
+
} catch (err) {
|
|
4754
|
+
process.stderr.write(`[atr] \u53C2\u6570\u89E3\u6790\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
|
|
4755
|
+
`);
|
|
4756
|
+
process.exit(2);
|
|
4757
|
+
}
|
|
4758
|
+
if (cli.help) {
|
|
4759
|
+
process.stdout.write(HELP_TEXT2);
|
|
4760
|
+
process.exit(0);
|
|
4761
|
+
}
|
|
4762
|
+
if (cli.version) {
|
|
4763
|
+
const { readFileSync: readFileSync7 } = await import("node:fs");
|
|
4764
|
+
const { resolve: resolve9, dirname: dirname5 } = await import("node:path");
|
|
4765
|
+
const { fileURLToPath: fileURLToPath3 } = await import("node:url");
|
|
4766
|
+
const __dirname2 = dirname5(fileURLToPath3(import.meta.url));
|
|
4767
|
+
const pkg = JSON.parse(readFileSync7(resolve9(__dirname2, "..", "package.json"), "utf-8"));
|
|
4768
|
+
process.stdout.write(`${pkg.version}
|
|
4769
|
+
`);
|
|
4770
|
+
process.exit(0);
|
|
4771
|
+
}
|
|
4772
|
+
if (cli.subcommand === "list") {
|
|
4773
|
+
const { listInstancesCli: listInstancesCli2 } = await Promise.resolve().then(() => (init_cli_list(), cli_list_exports));
|
|
4774
|
+
const code = await listInstancesCli2();
|
|
4775
|
+
process.exit(code);
|
|
4776
|
+
}
|
|
4777
|
+
if (cli.subcommand === "stop") {
|
|
4778
|
+
const { stopInstancesCli: stopInstancesCli2 } = await Promise.resolve().then(() => (init_cli_stop(), cli_stop_exports));
|
|
4779
|
+
const code = await stopInstancesCli2(cli.stopPattern);
|
|
4780
|
+
process.exit(code);
|
|
4781
|
+
}
|
|
4782
|
+
if (cli.subcommand === "attach") {
|
|
4783
|
+
if (!cli.attachUrl) {
|
|
4784
|
+
process.stderr.write("[atr] attach \u9700\u8981 URL \u53C2\u6570\n");
|
|
4785
|
+
process.exit(2);
|
|
4786
|
+
}
|
|
4787
|
+
const { runAttachCli: runAttachCli2 } = await Promise.resolve().then(() => (init_attach(), attach_exports));
|
|
4788
|
+
const code = await runAttachCli2(cli.attachUrl);
|
|
4789
|
+
process.exit(code);
|
|
4790
|
+
}
|
|
4791
|
+
await startServer2({ cli });
|
|
4792
|
+
})().catch((err) => {
|
|
4793
|
+
process.stderr.write(`[atr] \u542F\u52A8\u5931\u8D25\uFF1A${String(err)}
|
|
4794
|
+
`);
|
|
4795
|
+
if (err instanceof Error && err.stack) {
|
|
4796
|
+
process.stderr.write(`${err.stack}
|
|
4797
|
+
`);
|
|
4798
|
+
}
|
|
4799
|
+
process.exit(1);
|
|
4800
|
+
});
|