xshat-lite 1.0.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.
@@ -0,0 +1,257 @@
1
+ import fs from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import defaultConfig from '../config/default.mjs';
5
+ import {
6
+ readJsonSafeSync,
7
+ writeJsonAtomicSync
8
+ } from './storage.mjs';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+ const packageRootDir = resolve(__dirname, '..', '..');
13
+ const runtimeRootDir = process.env.XSHAT_HOME
14
+ ? resolve(process.env.XSHAT_HOME)
15
+ : packageRootDir;
16
+
17
+ function cloneProviders(providers) {
18
+ return providers.map((provider) => ({
19
+ ...provider,
20
+ enabled: provider.enabled !== false,
21
+ status: provider.status || 'experimental',
22
+ steps: {
23
+ preflight: {
24
+ ...(provider.steps?.preflight || {})
25
+ },
26
+ input: { ...(provider.steps?.input || {}) },
27
+ send: { ...(provider.steps?.send || {}) },
28
+ read: { ...(provider.steps?.read || {}) },
29
+ complete: {
30
+ ...(provider.steps?.complete || {}),
31
+ selectors: {
32
+ ...(provider.steps?.complete?.selectors || {})
33
+ }
34
+ }
35
+ }
36
+ }));
37
+ }
38
+
39
+ function deepMerge(base, override) {
40
+ if (!override || typeof override !== 'object' || Array.isArray(override)) {
41
+ return base;
42
+ }
43
+
44
+ const result = Array.isArray(base) ? [...base] : { ...base };
45
+
46
+ for (const [key, value] of Object.entries(override)) {
47
+ if (
48
+ value &&
49
+ typeof value === 'object' &&
50
+ !Array.isArray(value) &&
51
+ result[key] &&
52
+ typeof result[key] === 'object' &&
53
+ !Array.isArray(result[key])
54
+ ) {
55
+ result[key] = deepMerge(result[key], value);
56
+ continue;
57
+ }
58
+
59
+ result[key] = value;
60
+ }
61
+
62
+ return result;
63
+ }
64
+
65
+ const userConfigPath = resolve(runtimeRootDir, 'runtime', 'config', 'user.json');
66
+
67
+ function loadUserConfig() {
68
+ if (!fs.existsSync(userConfigPath)) {
69
+ return {};
70
+ }
71
+
72
+ return readJsonSafeSync(userConfigPath, {});
73
+ }
74
+
75
+ function buildBaseConfig() {
76
+ return {
77
+ ...defaultConfig,
78
+ providers: cloneProviders(defaultConfig.providers),
79
+ api: { ...(defaultConfig.api || {}) },
80
+ providersRuntime: { ...(defaultConfig.providersRuntime || {}) },
81
+ browser: { ...defaultConfig.browser },
82
+ chat: { ...defaultConfig.chat },
83
+ session: { ...defaultConfig.session },
84
+ tasks: { ...defaultConfig.tasks },
85
+ storage: { ...(defaultConfig.storage || {}) },
86
+ logger: { ...defaultConfig.logger },
87
+ agent: { ...(defaultConfig.agent || {}) },
88
+ debug: { ...(defaultConfig.debug || {}) }
89
+ };
90
+ }
91
+
92
+ function normalizeResolvedPaths(config) {
93
+ config.browser.userDataDir = resolve(runtimeRootDir, config.browser.userDataDir);
94
+ config.logger.dir = resolve(runtimeRootDir, config.logger.dir);
95
+ config.session.dir = resolve(runtimeRootDir, config.session.dir);
96
+ config.tasks.dir = resolve(runtimeRootDir, config.tasks.dir);
97
+ config.storage.archiveDir = resolve(runtimeRootDir, config.storage.archiveDir);
98
+ return config;
99
+ }
100
+
101
+ function loadConfig() {
102
+ const config = deepMerge(buildBaseConfig(), loadUserConfig());
103
+
104
+ if (process.env.DEFAULT_PROVIDER) {
105
+ config.defaultProvider = process.env.DEFAULT_PROVIDER;
106
+ }
107
+
108
+ if (process.env.BROWSER_HEADLESS) {
109
+ config.browser.headless = process.env.BROWSER_HEADLESS === 'true';
110
+ }
111
+
112
+ if (process.env.USER_DATA_DIR) {
113
+ config.browser.userDataDir = process.env.USER_DATA_DIR;
114
+ }
115
+
116
+ if (process.env.BROWSER_GOTO_TIMEOUT) {
117
+ config.browser.gotoTimeout = Number.parseInt(
118
+ process.env.BROWSER_GOTO_TIMEOUT,
119
+ 10
120
+ );
121
+ }
122
+
123
+ if (process.env.XSHAT_BROWSER_ALLOW_VISIBLE) {
124
+ config.browser.allowVisibleBrowser =
125
+ process.env.XSHAT_BROWSER_ALLOW_VISIBLE === 'true';
126
+ }
127
+
128
+ if (process.env.XSHAT_BROWSER_SHARE_AUTH_STATE) {
129
+ config.browser.shareAuthState =
130
+ process.env.XSHAT_BROWSER_SHARE_AUTH_STATE === 'true';
131
+ }
132
+
133
+ if (process.env.LOG_DIR) {
134
+ config.logger.dir = process.env.LOG_DIR;
135
+ }
136
+
137
+ if (process.env.SESSION_DIR) {
138
+ config.session.dir = process.env.SESSION_DIR;
139
+ }
140
+
141
+ if (process.env.TASK_DIR) {
142
+ config.tasks.dir = process.env.TASK_DIR;
143
+ }
144
+
145
+ if (process.env.XSHAT_DEBUG) {
146
+ config.debug.enabled = process.env.XSHAT_DEBUG === 'true';
147
+ }
148
+
149
+ if (process.env.XSHAT_AGENT_ENABLED) {
150
+ config.agent.enabled = process.env.XSHAT_AGENT_ENABLED === 'true';
151
+ }
152
+
153
+ if (process.env.XSHAT_AGENT_AUTO_EXECUTE) {
154
+ config.agent.autoExecuteCommands =
155
+ process.env.XSHAT_AGENT_AUTO_EXECUTE === 'true';
156
+ }
157
+
158
+ if (process.env.XSHAT_AGENT_ALLOW_SHELL) {
159
+ config.agent.allowShell = process.env.XSHAT_AGENT_ALLOW_SHELL === 'true';
160
+ }
161
+
162
+ if (process.env.XSHAT_AGENT_ALLOW_FILE_WRITE) {
163
+ config.agent.allowFileWrite =
164
+ process.env.XSHAT_AGENT_ALLOW_FILE_WRITE === 'true';
165
+ }
166
+
167
+ if (process.env.XSHAT_AGENT_MULTI_AGENT) {
168
+ config.agent.multiAgent = process.env.XSHAT_AGENT_MULTI_AGENT === 'true';
169
+ }
170
+
171
+ if (process.env.OPENAI_API_KEY) {
172
+ config.api.apiKey = process.env.OPENAI_API_KEY;
173
+ }
174
+
175
+ if (process.env.OPENAI_BASE_URL) {
176
+ config.api.baseUrl = process.env.OPENAI_BASE_URL;
177
+ }
178
+
179
+ if (process.env.OPENAI_MODEL) {
180
+ config.api.model = process.env.OPENAI_MODEL;
181
+ }
182
+
183
+ return normalizeResolvedPaths(config);
184
+ }
185
+
186
+ const config = loadConfig();
187
+ let userConfigOverrides = loadUserConfig();
188
+
189
+ function setNestedValue(target, path, value) {
190
+ const keys = Array.isArray(path) ? path : String(path).split('.').filter(Boolean);
191
+ if (keys.length === 0) {
192
+ return target;
193
+ }
194
+
195
+ let cursor = target;
196
+ for (let index = 0; index < keys.length - 1; index++) {
197
+ const key = keys[index];
198
+ if (!cursor[key] || typeof cursor[key] !== 'object' || Array.isArray(cursor[key])) {
199
+ cursor[key] = {};
200
+ }
201
+ cursor = cursor[key];
202
+ }
203
+
204
+ cursor[keys[keys.length - 1]] = value;
205
+ return target;
206
+ }
207
+
208
+ function refreshConfig() {
209
+ const nextConfig = loadConfig();
210
+
211
+ for (const key of Object.keys(config)) {
212
+ delete config[key];
213
+ }
214
+
215
+ Object.assign(config, nextConfig);
216
+ }
217
+
218
+ export function getUserConfigPath() {
219
+ return userConfigPath;
220
+ }
221
+
222
+ export function getUserConfigOverrides() {
223
+ return JSON.parse(JSON.stringify(userConfigOverrides));
224
+ }
225
+
226
+ export function saveConfigOverrides(overrides = userConfigOverrides) {
227
+ fs.mkdirSync(dirname(userConfigPath), { recursive: true });
228
+ writeJsonAtomicSync(userConfigPath, overrides);
229
+ userConfigOverrides = overrides;
230
+ refreshConfig();
231
+ return config;
232
+ }
233
+
234
+ export function updateConfigValue(path, value) {
235
+ const nextOverrides = getUserConfigOverrides();
236
+ setNestedValue(nextOverrides, path, value);
237
+ return saveConfigOverrides(nextOverrides);
238
+ }
239
+
240
+ export function listProviders({ includeDisabled = false } = {}) {
241
+ if (includeDisabled) {
242
+ return config.providers;
243
+ }
244
+
245
+ return config.providers.filter((provider) => provider.enabled !== false);
246
+ }
247
+
248
+ export function getProvider(id) {
249
+ const providerId = id || config.defaultProvider;
250
+ const provider = config.providers.find((item) => item.id === providerId);
251
+ if (!provider) {
252
+ throw new Error(`未找到 AI 提供方: ${providerId}`);
253
+ }
254
+ return provider;
255
+ }
256
+
257
+ export default config;
@@ -0,0 +1,86 @@
1
+ function normalizeChunk(chunk) {
2
+ const value = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk || '');
3
+
4
+ switch (value) {
5
+ case '\u0003':
6
+ return 'ctrl_c';
7
+ case '\r':
8
+ case '\n':
9
+ return 'enter';
10
+ case '\u001b':
11
+ return 'escape';
12
+ case '\u001b[A':
13
+ return 'up';
14
+ case '\u001b[B':
15
+ return 'down';
16
+ case '\u001b[C':
17
+ return 'right';
18
+ case '\u001b[D':
19
+ return 'left';
20
+ case '\u0008':
21
+ case '\u007f':
22
+ return 'backspace';
23
+ default:
24
+ if (value.length === 1) {
25
+ return value.toLowerCase();
26
+ }
27
+ return value.toLowerCase();
28
+ }
29
+ }
30
+
31
+ export async function waitForKeypress() {
32
+ return new Promise((resolve, reject) => {
33
+ const stdin = process.stdin;
34
+ const canUseRawMode = Boolean(stdin.isTTY && typeof stdin.setRawMode === 'function');
35
+
36
+ const cleanup = () => {
37
+ stdin.off('data', onData);
38
+ stdin.off('error', onError);
39
+ if (canUseRawMode) {
40
+ stdin.setRawMode(false);
41
+ }
42
+ stdin.pause();
43
+ };
44
+
45
+ const onError = (error) => {
46
+ cleanup();
47
+ reject(error);
48
+ };
49
+
50
+ const onData = (chunk) => {
51
+ const key = normalizeChunk(chunk);
52
+ cleanup();
53
+ if (key === 'ctrl_c') {
54
+ const error = new Error('AbortError');
55
+ error.name = 'AbortError';
56
+ reject(error);
57
+ return;
58
+ }
59
+ resolve(key);
60
+ };
61
+
62
+ if (canUseRawMode) {
63
+ stdin.setRawMode(true);
64
+ }
65
+ stdin.resume();
66
+ stdin.on('data', onData);
67
+ stdin.on('error', onError);
68
+ });
69
+ }
70
+
71
+ export function decodeKeypressChunk(chunk) {
72
+ const value = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk || '');
73
+ const key = normalizeChunk(value);
74
+ return {
75
+ key,
76
+ raw: value,
77
+ printable:
78
+ value.length === 1 &&
79
+ key !== 'enter' &&
80
+ key !== 'escape' &&
81
+ key !== 'ctrl_c' &&
82
+ key !== 'backspace'
83
+ ? value
84
+ : ''
85
+ };
86
+ }
@@ -0,0 +1,55 @@
1
+ import fs from 'node:fs';
2
+ import fsp from 'node:fs/promises';
3
+ import { dirname, resolve } from 'node:path';
4
+
5
+ function buildTempPath(targetPath) {
6
+ const suffix = `${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
7
+ return `${targetPath}.${suffix}.tmp`;
8
+ }
9
+
10
+ export async function writeTextAtomic(targetPath, content, encoding = 'utf-8') {
11
+ const directory = dirname(targetPath);
12
+ const tempPath = buildTempPath(targetPath);
13
+
14
+ await fsp.mkdir(directory, { recursive: true });
15
+ await fsp.writeFile(tempPath, content, encoding);
16
+ await fsp.rename(tempPath, targetPath);
17
+ }
18
+
19
+ export async function writeJsonAtomic(targetPath, value) {
20
+ const content = `${JSON.stringify(value, null, 2)}\n`;
21
+ await writeTextAtomic(targetPath, content, 'utf-8');
22
+ }
23
+
24
+ export function writeJsonAtomicSync(targetPath, value) {
25
+ const directory = dirname(targetPath);
26
+ const tempPath = buildTempPath(targetPath);
27
+ const content = `${JSON.stringify(value, null, 2)}\n`;
28
+
29
+ fs.mkdirSync(directory, { recursive: true });
30
+ fs.writeFileSync(tempPath, content, 'utf-8');
31
+ fs.renameSync(tempPath, targetPath);
32
+ }
33
+
34
+ export async function readJsonSafe(targetPath, fallback = null) {
35
+ try {
36
+ const raw = await fsp.readFile(targetPath, 'utf-8');
37
+ return JSON.parse(raw);
38
+ } catch {
39
+ return fallback;
40
+ }
41
+ }
42
+
43
+ export function readJsonSafeSync(targetPath, fallback = null) {
44
+ try {
45
+ const raw = fs.readFileSync(targetPath, 'utf-8');
46
+ return JSON.parse(raw);
47
+ } catch {
48
+ return fallback;
49
+ }
50
+ }
51
+
52
+ export function resolveSiblingPath(baseDir, name) {
53
+ return resolve(baseDir, name);
54
+ }
55
+