vibe-coding-master 0.3.4 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backend/adapters/filesystem.js +103 -29
- package/dist/backend/api/translation-routes.js +5 -0
- package/dist/backend/gateway/channels/weixin-ilink-channel.js +24 -5
- package/dist/backend/gateway/gateway-service.js +32 -8
- package/dist/backend/server.js +1 -1
- package/dist/backend/services/translation-service.js +4 -0
- package/dist-frontend/assets/{index-OPkFuHRf.js → index-j8A25Nt8.js} +2 -2
- package/dist-frontend/index.html +1 -1
- package/package.json +1 -1
|
@@ -1,58 +1,70 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
export function createNodeFileSystemAdapter() {
|
|
4
|
+
const runFileOperation = createFileOperationRunner();
|
|
4
5
|
return {
|
|
5
6
|
async pathExists(targetPath) {
|
|
6
7
|
try {
|
|
7
|
-
await fs.access(targetPath);
|
|
8
|
+
await runFileOperation(() => fs.access(targetPath));
|
|
8
9
|
return true;
|
|
9
10
|
}
|
|
10
|
-
catch {
|
|
11
|
-
|
|
11
|
+
catch (error) {
|
|
12
|
+
if (isMissingPathError(error)) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
throw error;
|
|
12
16
|
}
|
|
13
17
|
},
|
|
14
18
|
async ensureDir(targetPath) {
|
|
15
|
-
await fs.mkdir(targetPath, { recursive: true });
|
|
19
|
+
await runFileOperation(() => fs.mkdir(targetPath, { recursive: true }));
|
|
16
20
|
},
|
|
17
21
|
async readDir(targetPath) {
|
|
18
|
-
return fs.readdir(targetPath);
|
|
22
|
+
return runFileOperation(() => fs.readdir(targetPath));
|
|
19
23
|
},
|
|
20
24
|
async readText(targetPath) {
|
|
21
|
-
return fs.readFile(targetPath, "utf8");
|
|
25
|
+
return runFileOperation(() => fs.readFile(targetPath, "utf8"));
|
|
22
26
|
},
|
|
23
27
|
async readTextTail(targetPath, maxBytes) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
return runFileOperation(async () => {
|
|
29
|
+
const handle = await fs.open(targetPath, "r");
|
|
30
|
+
try {
|
|
31
|
+
const stat = await handle.stat();
|
|
32
|
+
const length = Math.min(Math.max(0, Math.floor(maxBytes)), stat.size);
|
|
33
|
+
const position = stat.size - length;
|
|
34
|
+
const buffer = Buffer.alloc(length);
|
|
35
|
+
await handle.read(buffer, 0, length, position);
|
|
36
|
+
return buffer.toString("utf8");
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
await handle.close();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
36
42
|
},
|
|
37
43
|
async writeText(targetPath, content) {
|
|
38
|
-
await
|
|
39
|
-
|
|
44
|
+
await runFileOperation(async () => {
|
|
45
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
46
|
+
await fs.writeFile(targetPath, content, "utf8");
|
|
47
|
+
});
|
|
40
48
|
},
|
|
41
49
|
async appendText(targetPath, content) {
|
|
42
|
-
await
|
|
43
|
-
|
|
50
|
+
await runFileOperation(async () => {
|
|
51
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
52
|
+
await fs.appendFile(targetPath, content, "utf8");
|
|
53
|
+
});
|
|
44
54
|
},
|
|
45
55
|
async readJson(targetPath) {
|
|
46
|
-
return JSON.parse(await fs.readFile(targetPath, "utf8"));
|
|
56
|
+
return JSON.parse(await runFileOperation(() => fs.readFile(targetPath, "utf8")));
|
|
47
57
|
},
|
|
48
58
|
async writeJson(targetPath, value) {
|
|
49
59
|
await this.writeText(targetPath, `${JSON.stringify(value, null, 2)}\n`);
|
|
50
60
|
},
|
|
51
61
|
async writeJsonAtomic(targetPath, value) {
|
|
52
|
-
await
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
await runFileOperation(async () => {
|
|
63
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
64
|
+
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`;
|
|
65
|
+
await fs.writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
66
|
+
await fs.rename(tempPath, targetPath);
|
|
67
|
+
});
|
|
56
68
|
},
|
|
57
69
|
async ensureFile(targetPath, content, options = {}) {
|
|
58
70
|
if (!options.overwrite && await this.pathExists(targetPath)) {
|
|
@@ -62,13 +74,75 @@ export function createNodeFileSystemAdapter() {
|
|
|
62
74
|
return true;
|
|
63
75
|
},
|
|
64
76
|
async removePath(targetPath, options = {}) {
|
|
65
|
-
await fs.rm(targetPath, {
|
|
77
|
+
await runFileOperation(() => fs.rm(targetPath, {
|
|
66
78
|
recursive: options.recursive ?? false,
|
|
67
79
|
force: options.force ?? false
|
|
68
|
-
});
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const DEFAULT_FILE_OPERATION_CONCURRENCY = 32;
|
|
85
|
+
const OPEN_FILE_RETRY_DELAYS_MS = [25, 50, 100, 200, 400, 800];
|
|
86
|
+
function createFileOperationRunner(maxConcurrent = DEFAULT_FILE_OPERATION_CONCURRENCY) {
|
|
87
|
+
let active = 0;
|
|
88
|
+
const waiters = [];
|
|
89
|
+
async function acquire() {
|
|
90
|
+
if (active < maxConcurrent) {
|
|
91
|
+
active += 1;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
await new Promise((resolve) => waiters.push(resolve));
|
|
95
|
+
}
|
|
96
|
+
function release() {
|
|
97
|
+
const next = waiters.shift();
|
|
98
|
+
if (next) {
|
|
99
|
+
next();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
active = Math.max(0, active - 1);
|
|
103
|
+
}
|
|
104
|
+
return async (operation) => {
|
|
105
|
+
await acquire();
|
|
106
|
+
try {
|
|
107
|
+
return await retryOpenFileLimit(operation);
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
release();
|
|
69
111
|
}
|
|
70
112
|
};
|
|
71
113
|
}
|
|
114
|
+
async function retryOpenFileLimit(operation) {
|
|
115
|
+
let attempt = 0;
|
|
116
|
+
for (;;) {
|
|
117
|
+
try {
|
|
118
|
+
return await operation();
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
const delayMs = OPEN_FILE_RETRY_DELAYS_MS[attempt];
|
|
122
|
+
if (!isOpenFileLimitError(error) || delayMs === undefined) {
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
attempt += 1;
|
|
126
|
+
await delay(delayMs);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function isOpenFileLimitError(error) {
|
|
131
|
+
const code = getErrorCode(error);
|
|
132
|
+
return code === "EMFILE" || code === "ENFILE";
|
|
133
|
+
}
|
|
134
|
+
function isMissingPathError(error) {
|
|
135
|
+
const code = getErrorCode(error);
|
|
136
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
137
|
+
}
|
|
138
|
+
function getErrorCode(error) {
|
|
139
|
+
return typeof error === "object" && error !== null && "code" in error
|
|
140
|
+
? String(error.code)
|
|
141
|
+
: undefined;
|
|
142
|
+
}
|
|
143
|
+
function delay(ms) {
|
|
144
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
145
|
+
}
|
|
72
146
|
export function resolveRepoPath(repoRoot, repoRelativePath) {
|
|
73
147
|
if (path.isAbsolute(repoRelativePath)) {
|
|
74
148
|
return repoRelativePath;
|
|
@@ -60,6 +60,11 @@ export function registerTranslationRoutes(app, deps) {
|
|
|
60
60
|
await deps.translationService.clearSession(request.params.sessionId);
|
|
61
61
|
return { ok: true };
|
|
62
62
|
});
|
|
63
|
+
app.post("/api/translation/sessions/:sessionId/stop", async (request) => {
|
|
64
|
+
await requireCurrentProject(deps.projectService);
|
|
65
|
+
await deps.translationService.stopSession(request.params.sessionId);
|
|
66
|
+
return { ok: true };
|
|
67
|
+
});
|
|
63
68
|
app.post("/api/translation/sessions/:sessionId/retry/:translationId", async (request) => {
|
|
64
69
|
await requireCurrentProject(deps.projectService);
|
|
65
70
|
return deps.translationService.retryTranslation(request.params.sessionId, request.params.translationId);
|
|
@@ -47,14 +47,14 @@ export function createWeixinIlinkChannel(options = {}) {
|
|
|
47
47
|
async function fetchJson(input) {
|
|
48
48
|
const controller = input.timeoutMs !== undefined ? new AbortController() : undefined;
|
|
49
49
|
const timeout = controller ? setTimeout(() => controller.abort(), input.timeoutMs) : undefined;
|
|
50
|
-
const
|
|
50
|
+
const combinedSignal = combineSignals(controller?.signal, input.signal);
|
|
51
51
|
const url = new URL(input.endpoint, ensureTrailingSlash(input.requestBaseUrl));
|
|
52
52
|
try {
|
|
53
53
|
const response = await fetchImpl(url, {
|
|
54
54
|
method: input.method,
|
|
55
55
|
headers: input.jsonHeaders === false ? buildIlinkAppHeaders() : buildJsonHeaders(input.token),
|
|
56
56
|
body: input.body === undefined ? undefined : JSON.stringify(input.body),
|
|
57
|
-
signal
|
|
57
|
+
signal: combinedSignal.signal
|
|
58
58
|
});
|
|
59
59
|
const rawText = await response.text();
|
|
60
60
|
if (!response.ok) {
|
|
@@ -66,6 +66,7 @@ export function createWeixinIlinkChannel(options = {}) {
|
|
|
66
66
|
if (timeout) {
|
|
67
67
|
clearTimeout(timeout);
|
|
68
68
|
}
|
|
69
|
+
combinedSignal.cleanup();
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
72
|
async function post(input) {
|
|
@@ -285,10 +286,22 @@ function randomWechatUin() {
|
|
|
285
286
|
}
|
|
286
287
|
function combineSignals(first, second) {
|
|
287
288
|
if (!first) {
|
|
288
|
-
return
|
|
289
|
+
return {
|
|
290
|
+
signal: second,
|
|
291
|
+
cleanup() { }
|
|
292
|
+
};
|
|
289
293
|
}
|
|
290
294
|
if (!second) {
|
|
291
|
-
return
|
|
295
|
+
return {
|
|
296
|
+
signal: first,
|
|
297
|
+
cleanup() { }
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
if (first === second) {
|
|
301
|
+
return {
|
|
302
|
+
signal: first,
|
|
303
|
+
cleanup() { }
|
|
304
|
+
};
|
|
292
305
|
}
|
|
293
306
|
const controller = new AbortController();
|
|
294
307
|
const abort = () => controller.abort();
|
|
@@ -297,7 +310,13 @@ function combineSignals(first, second) {
|
|
|
297
310
|
if (first.aborted || second.aborted) {
|
|
298
311
|
controller.abort();
|
|
299
312
|
}
|
|
300
|
-
return
|
|
313
|
+
return {
|
|
314
|
+
signal: controller.signal,
|
|
315
|
+
cleanup() {
|
|
316
|
+
first.removeEventListener("abort", abort);
|
|
317
|
+
second.removeEventListener("abort", abort);
|
|
318
|
+
}
|
|
319
|
+
};
|
|
301
320
|
}
|
|
302
321
|
function stringOrUndefined(value) {
|
|
303
322
|
return typeof value === "string" ? value : undefined;
|
|
@@ -25,28 +25,52 @@ export function createGatewayService(deps) {
|
|
|
25
25
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
26
26
|
let pollAbort = null;
|
|
27
27
|
let pollLoopPromise = null;
|
|
28
|
+
let pollStartingPromise = null;
|
|
28
29
|
let qrLogin = null;
|
|
29
30
|
let lastFailedTranslation = null;
|
|
30
31
|
function isRunning() {
|
|
31
32
|
return Boolean(pollAbort && !pollAbort.signal.aborted);
|
|
32
33
|
}
|
|
33
34
|
async function ensurePolling() {
|
|
34
|
-
|
|
35
|
-
if (!settings.binding.token || isRunning()) {
|
|
35
|
+
if (isRunning()) {
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
if (pollStartingPromise) {
|
|
39
|
+
await pollStartingPromise;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
pollStartingPromise = (async () => {
|
|
43
|
+
const settings = await deps.settings.loadSettings();
|
|
44
|
+
if (!settings.binding.token || isRunning()) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const controller = new AbortController();
|
|
48
|
+
pollAbort = controller;
|
|
49
|
+
const loop = pollLoop(controller.signal).finally(() => {
|
|
50
|
+
if (pollAbort === controller) {
|
|
51
|
+
pollAbort = null;
|
|
52
|
+
}
|
|
53
|
+
if (pollLoopPromise === loop) {
|
|
54
|
+
pollLoopPromise = null;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
pollLoopPromise = loop;
|
|
58
|
+
})().finally(() => {
|
|
59
|
+
pollStartingPromise = null;
|
|
42
60
|
});
|
|
61
|
+
await pollStartingPromise;
|
|
43
62
|
}
|
|
44
63
|
async function stopPolling() {
|
|
64
|
+
await pollStartingPromise?.catch(() => undefined);
|
|
45
65
|
if (!pollAbort) {
|
|
46
66
|
return;
|
|
47
67
|
}
|
|
48
|
-
pollAbort
|
|
68
|
+
const controller = pollAbort;
|
|
69
|
+
controller.abort();
|
|
49
70
|
await pollLoopPromise?.catch(() => undefined);
|
|
71
|
+
if (pollAbort === controller) {
|
|
72
|
+
pollAbort = null;
|
|
73
|
+
}
|
|
50
74
|
}
|
|
51
75
|
function toAccount(settings) {
|
|
52
76
|
if (!settings.binding.token) {
|
|
@@ -643,7 +667,7 @@ export function createGatewayService(deps) {
|
|
|
643
667
|
await ensurePolling();
|
|
644
668
|
},
|
|
645
669
|
stop() {
|
|
646
|
-
|
|
670
|
+
return stopPolling();
|
|
647
671
|
},
|
|
648
672
|
async getStatus() {
|
|
649
673
|
const settings = await syncDesktopContext(await deps.settings.loadSettings());
|
package/dist/backend/server.js
CHANGED
|
@@ -111,7 +111,7 @@ export async function createServer(deps, options = {}) {
|
|
|
111
111
|
await deps.gatewayService.start();
|
|
112
112
|
});
|
|
113
113
|
app.addHook("onClose", async () => {
|
|
114
|
-
deps.gatewayService.stop();
|
|
114
|
+
await deps.gatewayService.stop();
|
|
115
115
|
});
|
|
116
116
|
if (options.staticDir) {
|
|
117
117
|
await app.register(fastifyStatic, {
|
|
@@ -703,6 +703,10 @@ export function createTranslationService(deps) {
|
|
|
703
703
|
});
|
|
704
704
|
return () => {
|
|
705
705
|
state.listeners.delete(listener);
|
|
706
|
+
if (state.listeners.size === 0 && state.unsubscribeTranscript) {
|
|
707
|
+
state.unsubscribeTranscript();
|
|
708
|
+
state.unsubscribeTranscript = undefined;
|
|
709
|
+
}
|
|
706
710
|
};
|
|
707
711
|
},
|
|
708
712
|
async clearSession(sessionId) {
|