dsh-client-auto-continue 0.7.4 → 0.8.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/README.md +8 -12
- package/README.zh.md +8 -12
- package/lib/client.js +208 -1134
- package/lib/client.js.map +4 -4
- package/lib/index.js +993 -28
- package/lib/types/client/bridge.d.ts +39 -0
- package/lib/types/client/engine.d.ts +7 -2
- package/lib/types/client/index.d.ts +11 -14
- package/lib/types/host/engine.d.ts +258 -0
- package/lib/types/index.d.ts +11 -5
- package/package.json +4 -3
- package/src/client/bridge.ts +202 -0
- package/src/client/engine.ts +34 -4
- package/src/client/index.ts +19 -29
- package/src/client/settings-card.tsx +9 -4
- package/src/host/engine.ts +1270 -0
- package/src/index.ts +97 -5
- package/tsconfig.json +2 -1
package/lib/client.js
CHANGED
|
@@ -27,1106 +27,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
27
27
|
var index_exports = {};
|
|
28
28
|
__export(index_exports, {
|
|
29
29
|
apply: () => apply,
|
|
30
|
-
fillTemplate: () => fillTemplate,
|
|
31
30
|
inject: () => inject,
|
|
32
|
-
pauseSession: () => pauseSession,
|
|
33
31
|
pausedSessions: () => pausedSessions,
|
|
34
32
|
readTodayStats: () => readTodayStats,
|
|
35
33
|
resetTodayStats: () => resetTodayStats,
|
|
36
|
-
sessionPauseUntil: () => sessionPauseUntil,
|
|
37
34
|
unpauseSession: () => unpauseSession
|
|
38
35
|
});
|
|
39
36
|
module.exports = __toCommonJS(index_exports);
|
|
40
37
|
|
|
41
|
-
// src/client/engine.ts
|
|
42
|
-
var DEFAULT_CONFIG = {
|
|
43
|
-
continueText: "继续",
|
|
44
|
-
continueTextMaxTokens: "继续",
|
|
45
|
-
guardTools: true,
|
|
46
|
-
guardPendingText: "(上一步工具「{tool}」可能未完成, 先确认状态再继续, 不要重复执行)",
|
|
47
|
-
guardDoneText: "(上一步工具「{tool}」已完成, 结果: {result}; 不要重复执行, 直接继续)",
|
|
48
|
-
graceMs: 3e3,
|
|
49
|
-
cooldownMs: 2e4,
|
|
50
|
-
maxConsecutive: 3,
|
|
51
|
-
scanOnBoot: true,
|
|
52
|
-
scanLimit: 8,
|
|
53
|
-
freshMs: 15 * 60 * 1e3,
|
|
54
|
-
reconnectScanDelayMs: 5e3,
|
|
55
|
-
reconnectBackoffMs: 3e3,
|
|
56
|
-
verbose: true,
|
|
57
|
-
classify: true,
|
|
58
|
-
backoffFactor: 2,
|
|
59
|
-
backoffMaxMs: 3e5,
|
|
60
|
-
notify: false,
|
|
61
|
-
paused: false,
|
|
62
|
-
loopGuard: true,
|
|
63
|
-
loopShortChars: 40,
|
|
64
|
-
loopWindowMs: 3e4,
|
|
65
|
-
loopShortCount: 12,
|
|
66
|
-
loopRepeatText: 4,
|
|
67
|
-
loopToolRepeat: 5,
|
|
68
|
-
loopText: "(检测到你可能陷入循环, 请停止重复刚才的动作, 换一种方式继续)"
|
|
69
|
-
};
|
|
70
|
-
function numberOr(value, fallback) {
|
|
71
|
-
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
72
|
-
}
|
|
73
|
-
function booleanOr(value, fallback) {
|
|
74
|
-
return typeof value === "boolean" ? value : fallback;
|
|
75
|
-
}
|
|
76
|
-
function resolveConfig(section) {
|
|
77
|
-
const value = section ?? {};
|
|
78
|
-
const text = typeof value.continueText === "string" && value.continueText.trim() !== "" ? value.continueText : DEFAULT_CONFIG.continueText;
|
|
79
|
-
const maxTokensText = typeof value.continueTextMaxTokens === "string" && value.continueTextMaxTokens.trim() !== "" ? value.continueTextMaxTokens : DEFAULT_CONFIG.continueTextMaxTokens;
|
|
80
|
-
const guardPendingText = typeof value.guardPendingText === "string" && value.guardPendingText.trim() !== "" ? value.guardPendingText : DEFAULT_CONFIG.guardPendingText;
|
|
81
|
-
const guardDoneText = typeof value.guardDoneText === "string" && value.guardDoneText.trim() !== "" ? value.guardDoneText : DEFAULT_CONFIG.guardDoneText;
|
|
82
|
-
return {
|
|
83
|
-
continueText: text,
|
|
84
|
-
continueTextMaxTokens: maxTokensText,
|
|
85
|
-
guardTools: booleanOr(value.guardTools, DEFAULT_CONFIG.guardTools),
|
|
86
|
-
guardPendingText,
|
|
87
|
-
guardDoneText,
|
|
88
|
-
graceMs: numberOr(value.graceMs, DEFAULT_CONFIG.graceMs),
|
|
89
|
-
cooldownMs: numberOr(value.cooldownMs, DEFAULT_CONFIG.cooldownMs),
|
|
90
|
-
maxConsecutive: Math.max(1, numberOr(value.maxConsecutive, DEFAULT_CONFIG.maxConsecutive)),
|
|
91
|
-
scanOnBoot: booleanOr(value.scanOnBoot, DEFAULT_CONFIG.scanOnBoot),
|
|
92
|
-
scanLimit: Math.max(1, numberOr(value.scanLimit, DEFAULT_CONFIG.scanLimit)),
|
|
93
|
-
freshMs: numberOr(value.freshMs, DEFAULT_CONFIG.freshMs),
|
|
94
|
-
reconnectScanDelayMs: numberOr(value.reconnectScanDelayMs, DEFAULT_CONFIG.reconnectScanDelayMs),
|
|
95
|
-
reconnectBackoffMs: numberOr(value.reconnectBackoffMs, DEFAULT_CONFIG.reconnectBackoffMs),
|
|
96
|
-
verbose: booleanOr(value.verbose, DEFAULT_CONFIG.verbose),
|
|
97
|
-
classify: booleanOr(value.classify, DEFAULT_CONFIG.classify),
|
|
98
|
-
backoffFactor: Math.max(1, numberOr(value.backoffFactor, DEFAULT_CONFIG.backoffFactor)),
|
|
99
|
-
backoffMaxMs: numberOr(value.backoffMaxMs, DEFAULT_CONFIG.backoffMaxMs),
|
|
100
|
-
notify: booleanOr(value.notify, DEFAULT_CONFIG.notify),
|
|
101
|
-
paused: booleanOr(value.paused, DEFAULT_CONFIG.paused),
|
|
102
|
-
loopGuard: booleanOr(value.loopGuard, DEFAULT_CONFIG.loopGuard),
|
|
103
|
-
loopShortChars: Math.max(1, numberOr(value.loopShortChars, DEFAULT_CONFIG.loopShortChars)),
|
|
104
|
-
loopWindowMs: Math.max(1e3, numberOr(value.loopWindowMs, DEFAULT_CONFIG.loopWindowMs)),
|
|
105
|
-
loopShortCount: Math.max(2, numberOr(value.loopShortCount, DEFAULT_CONFIG.loopShortCount)),
|
|
106
|
-
loopRepeatText: Math.max(2, numberOr(value.loopRepeatText, DEFAULT_CONFIG.loopRepeatText)),
|
|
107
|
-
loopToolRepeat: Math.max(2, numberOr(value.loopToolRepeat, DEFAULT_CONFIG.loopToolRepeat)),
|
|
108
|
-
loopText: typeof value.loopText === "string" && value.loopText.trim() !== "" ? value.loopText : DEFAULT_CONFIG.loopText
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
function isNonHumanReason(kind) {
|
|
112
|
-
return kind === "error" || kind === "interrupted" || kind === "max-tokens";
|
|
113
|
-
}
|
|
114
|
-
function isTransientFailure(failure) {
|
|
115
|
-
const haystack = `${failure.code} ${failure.message}`.toLowerCase();
|
|
116
|
-
const status = failure.status;
|
|
117
|
-
if (status !== void 0 && (status === 401 || status === 403)) return false;
|
|
118
|
-
const permanent = /auth|unauthor|forbidden|credential|api[_-]?key|permission/i.test(haystack) || /insufficient.*(balance|quota)|billing|payment|quota.*exceeded.*(?!retry)/i.test(haystack) || /model.*not[_-]?found|unknown[_-]?model|model[_-]?not[_-]?found|not.*support.*model/i.test(haystack) || /context.*(length|limit|overflow|exceed)|token.*limit|max.*context/i.test(haystack) || /invalid[_-]?request|bad[_-]?request/i.test(haystack);
|
|
119
|
-
return !permanent;
|
|
120
|
-
}
|
|
121
|
-
function isTransientAgentError(message) {
|
|
122
|
-
return /network|timeout|timed ?out|econn|etimedout|socket|5\d\d|\b429\b|upstream|temporar/i.test(message);
|
|
123
|
-
}
|
|
124
|
-
function notify(title, body, options) {
|
|
125
|
-
try {
|
|
126
|
-
const N = globalThis.Notification;
|
|
127
|
-
if (typeof N === "undefined") return;
|
|
128
|
-
const permission = N.permission;
|
|
129
|
-
const create = () => {
|
|
130
|
-
const instance = new N(title, {
|
|
131
|
-
body,
|
|
132
|
-
...options?.actions !== void 0 && options.actions.length > 0 ? { actions: options.actions } : {}
|
|
133
|
-
});
|
|
134
|
-
const target = instance;
|
|
135
|
-
target.onclick = () => {
|
|
136
|
-
try {
|
|
137
|
-
globalThis.focus?.();
|
|
138
|
-
} catch {
|
|
139
|
-
}
|
|
140
|
-
};
|
|
141
|
-
if (options?.onAction !== void 0) {
|
|
142
|
-
target.onaction = (event) => options.onAction?.(event.action);
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
if (permission === "granted") {
|
|
146
|
-
create();
|
|
147
|
-
} else if (permission === "default") {
|
|
148
|
-
void N.requestPermission?.().then((result) => {
|
|
149
|
-
if (result === "granted") create();
|
|
150
|
-
}).catch(() => {
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
} catch {
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
function formatElapsed(ms) {
|
|
157
|
-
if (ms === void 0 || !Number.isFinite(ms) || ms < 0) return "";
|
|
158
|
-
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
159
|
-
const s = Math.round(ms / 1e3);
|
|
160
|
-
if (s < 60) return `${s}s`;
|
|
161
|
-
return `${Math.floor(s / 60)}m${s % 60 > 0 ? `${s % 60}s` : ""}`;
|
|
162
|
-
}
|
|
163
|
-
function fillTemplate(template, ctx) {
|
|
164
|
-
return template.replace(/\{code\}/g, ctx.facts?.code ?? "").replace(/\{message\}/g, ctx.facts?.message ?? "").replace(/\{status\}/g, ctx.facts?.status !== void 0 ? String(ctx.facts.status) : "").replace(/\{tool\}/g, ctx.tool ?? "").replace(/\{turn\}/g, ctx.turn !== void 0 ? String(ctx.turn) : "").replace(/\{errorCount\}/g, ctx.errorCount !== void 0 ? String(ctx.errorCount) : "").replace(/\{sessionTitle\}/g, ctx.sessionTitle ?? "").replace(/\{elapsed\}/g, formatElapsed(ctx.elapsedMs)).replace(/\{result\}/g, ctx.result ?? "");
|
|
165
|
-
}
|
|
166
|
-
var TOOL_RESULT_CAP = 160;
|
|
167
|
-
function extractText(blocks, cap) {
|
|
168
|
-
let out = "";
|
|
169
|
-
const walk = (value) => {
|
|
170
|
-
if (out.length >= cap) return;
|
|
171
|
-
if (Array.isArray(value)) {
|
|
172
|
-
for (const item of value) walk(item);
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
if (typeof value !== "object" || value === null) return;
|
|
176
|
-
const record = value;
|
|
177
|
-
if (record["type"] === "text" && typeof record["text"] === "string") {
|
|
178
|
-
out += record["text"];
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
for (const child of Object.values(record)) walk(child);
|
|
182
|
-
};
|
|
183
|
-
walk(blocks);
|
|
184
|
-
return out.slice(0, cap);
|
|
185
|
-
}
|
|
186
|
-
function toolResultFacts(data) {
|
|
187
|
-
const failed = data.error !== void 0 || data.message?.content?.[0]?.isError === true;
|
|
188
|
-
return { ok: !failed, excerpt: extractText(data.message?.content?.[0]?.content, TOOL_RESULT_CAP) };
|
|
189
|
-
}
|
|
190
|
-
function effectiveCooldown(consecutive, base, factor, max) {
|
|
191
|
-
const multiplier = Math.pow(factor, consecutive);
|
|
192
|
-
return Math.min(Math.max(base, base * multiplier), Math.max(base, max));
|
|
193
|
-
}
|
|
194
|
-
function sleep(ms) {
|
|
195
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
196
|
-
}
|
|
197
|
-
function clientTimeZone() {
|
|
198
|
-
try {
|
|
199
|
-
return Intl.DateTimeFormat().resolvedOptions().timeZone || void 0;
|
|
200
|
-
} catch {
|
|
201
|
-
return void 0;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
var lockPrefix = "dsh-auto-continue:";
|
|
205
|
-
var lockKey = (sessionId) => `${lockPrefix}lock:${sessionId}`;
|
|
206
|
-
var stampKey = (sessionId) => `${lockPrefix}last:${sessionId}`;
|
|
207
|
-
var countKey = (sessionId) => `${lockPrefix}count:${sessionId}`;
|
|
208
|
-
function readLastSent(sessionId) {
|
|
209
|
-
try {
|
|
210
|
-
const raw = localStorage.getItem(stampKey(sessionId));
|
|
211
|
-
if (raw === null) return { at: 0, text: "" };
|
|
212
|
-
const parsed = JSON.parse(raw);
|
|
213
|
-
if (typeof parsed === "object" && parsed !== null && typeof parsed.text === "string") {
|
|
214
|
-
return { at: Number(parsed.at) || 0, text: parsed.text };
|
|
215
|
-
}
|
|
216
|
-
return { at: Number(raw) || 0, text: "" };
|
|
217
|
-
} catch {
|
|
218
|
-
return { at: 0, text: "" };
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
function readLastSend(sessionId) {
|
|
222
|
-
return readLastSent(sessionId).at;
|
|
223
|
-
}
|
|
224
|
-
function writeLastSend(sessionId, at, text) {
|
|
225
|
-
try {
|
|
226
|
-
localStorage.setItem(stampKey(sessionId), JSON.stringify({ at, text }));
|
|
227
|
-
} catch {
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
var SEND_COUNT_WINDOW_MS = 10 * 60 * 1e3;
|
|
231
|
-
function readSendCount(sessionId) {
|
|
232
|
-
try {
|
|
233
|
-
const raw = localStorage.getItem(countKey(sessionId));
|
|
234
|
-
if (raw === null) return { at: 0, count: 0 };
|
|
235
|
-
const parsed = JSON.parse(raw);
|
|
236
|
-
if (typeof parsed === "object" && parsed !== null && typeof parsed.count === "number") {
|
|
237
|
-
const at = Number(parsed.at) || 0;
|
|
238
|
-
if (Date.now() - at > SEND_COUNT_WINDOW_MS) return { at: 0, count: 0 };
|
|
239
|
-
return { at, count: parsed.count };
|
|
240
|
-
}
|
|
241
|
-
} catch {
|
|
242
|
-
}
|
|
243
|
-
return { at: 0, count: 0 };
|
|
244
|
-
}
|
|
245
|
-
function bumpSendCount(sessionId) {
|
|
246
|
-
try {
|
|
247
|
-
const current2 = readSendCount(sessionId);
|
|
248
|
-
localStorage.setItem(
|
|
249
|
-
countKey(sessionId),
|
|
250
|
-
JSON.stringify({ at: Date.now(), count: current2.count + 1 })
|
|
251
|
-
);
|
|
252
|
-
} catch {
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
function clearSendCount(sessionId) {
|
|
256
|
-
try {
|
|
257
|
-
localStorage.removeItem(countKey(sessionId));
|
|
258
|
-
} catch {
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
async function withSendLock(sessionId, body) {
|
|
262
|
-
const nav = globalThis.navigator;
|
|
263
|
-
if (nav?.locks !== void 0) {
|
|
264
|
-
await nav.locks.request(`dsh-auto-continue:send:${sessionId}`, body);
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
if (!claimSend(sessionId)) return;
|
|
268
|
-
try {
|
|
269
|
-
await body();
|
|
270
|
-
} finally {
|
|
271
|
-
releaseSend(sessionId);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
function claimSend(sessionId) {
|
|
275
|
-
try {
|
|
276
|
-
const token = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
277
|
-
localStorage.setItem(lockKey(sessionId), token);
|
|
278
|
-
return localStorage.getItem(lockKey(sessionId)) === token;
|
|
279
|
-
} catch {
|
|
280
|
-
return true;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
function releaseSend(sessionId) {
|
|
284
|
-
try {
|
|
285
|
-
localStorage.removeItem(lockKey(sessionId));
|
|
286
|
-
} catch {
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
var pauseKey = (sessionId) => `${lockPrefix}pause:${sessionId}`;
|
|
290
|
-
function pauseSession(sessionId, ms) {
|
|
291
|
-
try {
|
|
292
|
-
localStorage.setItem(pauseKey(sessionId), String(Date.now() + ms));
|
|
293
|
-
} catch {
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
function unpauseSession(sessionId) {
|
|
297
|
-
try {
|
|
298
|
-
localStorage.removeItem(pauseKey(sessionId));
|
|
299
|
-
} catch {
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
function sessionPauseUntil(sessionId) {
|
|
303
|
-
try {
|
|
304
|
-
return Number(localStorage.getItem(pauseKey(sessionId)) ?? 0) || 0;
|
|
305
|
-
} catch {
|
|
306
|
-
return 0;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
function pausedSessions() {
|
|
310
|
-
const out = [];
|
|
311
|
-
const now = Date.now();
|
|
312
|
-
try {
|
|
313
|
-
for (let i = 0; i < localStorage.length; i += 1) {
|
|
314
|
-
const key = localStorage.key(i);
|
|
315
|
-
if (key === null || !key.startsWith(`${lockPrefix}pause:`)) continue;
|
|
316
|
-
const sessionId = key.slice(lockPrefix.length + "pause:".length);
|
|
317
|
-
const until = Number(localStorage.getItem(key) ?? 0) || 0;
|
|
318
|
-
if (until > now) out.push({ sessionId, until });
|
|
319
|
-
else localStorage.removeItem(key);
|
|
320
|
-
}
|
|
321
|
-
} catch {
|
|
322
|
-
}
|
|
323
|
-
return out;
|
|
324
|
-
}
|
|
325
|
-
var statsKey = `${lockPrefix}stats`;
|
|
326
|
-
var STATS_MAX_DAYS = 90;
|
|
327
|
-
function todayKey() {
|
|
328
|
-
const d = /* @__PURE__ */ new Date();
|
|
329
|
-
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
330
|
-
const dd = String(d.getDate()).padStart(2, "0");
|
|
331
|
-
return `${d.getFullYear()}-${mm}-${dd}`;
|
|
332
|
-
}
|
|
333
|
-
function readStats() {
|
|
334
|
-
try {
|
|
335
|
-
const raw = localStorage.getItem(statsKey);
|
|
336
|
-
if (raw === null) return [];
|
|
337
|
-
const parsed = JSON.parse(raw);
|
|
338
|
-
if (!Array.isArray(parsed)) return [];
|
|
339
|
-
return parsed.filter(
|
|
340
|
-
(item) => typeof item === "object" && item !== null && typeof item.date === "string"
|
|
341
|
-
);
|
|
342
|
-
} catch {
|
|
343
|
-
return [];
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
function writeStats(list) {
|
|
347
|
-
try {
|
|
348
|
-
localStorage.setItem(statsKey, JSON.stringify(list));
|
|
349
|
-
} catch {
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
function bumpStat(delta) {
|
|
353
|
-
const list = readStats();
|
|
354
|
-
let day = list.find((item) => item.date === todayKey());
|
|
355
|
-
if (day === void 0) {
|
|
356
|
-
day = { date: todayKey(), sent: 0, skipped: 0, recovered: 0, failed: 0, gaveUp: 0, looped: 0, byCode: {} };
|
|
357
|
-
list.unshift(day);
|
|
358
|
-
}
|
|
359
|
-
if (delta.sent !== void 0) day.sent += delta.sent;
|
|
360
|
-
if (delta.skipped !== void 0) day.skipped += delta.skipped;
|
|
361
|
-
if (delta.recovered !== void 0) day.recovered += delta.recovered;
|
|
362
|
-
if (delta.failed !== void 0) day.failed += delta.failed;
|
|
363
|
-
if (delta.gaveUp !== void 0) day.gaveUp += delta.gaveUp;
|
|
364
|
-
if (delta.looped !== void 0) day.looped += delta.looped;
|
|
365
|
-
if (delta.code !== void 0) day.byCode[delta.code] = (day.byCode[delta.code] ?? 0) + 1;
|
|
366
|
-
writeStats(list.slice(0, STATS_MAX_DAYS));
|
|
367
|
-
}
|
|
368
|
-
function readTodayStats() {
|
|
369
|
-
const today = todayKey();
|
|
370
|
-
const found = readStats().find((item) => item.date === today);
|
|
371
|
-
return found ?? { date: today, sent: 0, skipped: 0, recovered: 0, failed: 0, gaveUp: 0, looped: 0, byCode: {} };
|
|
372
|
-
}
|
|
373
|
-
function resetTodayStats() {
|
|
374
|
-
writeStats(readStats().filter((item) => item.date !== todayKey()));
|
|
375
|
-
}
|
|
376
|
-
var freshState = () => ({
|
|
377
|
-
consecutive: 0,
|
|
378
|
-
lastAutoAt: 0,
|
|
379
|
-
lastAttemptAt: 0,
|
|
380
|
-
lastSentText: "",
|
|
381
|
-
pendingTimer: void 0,
|
|
382
|
-
running: void 0,
|
|
383
|
-
queued: 0,
|
|
384
|
-
subagent: false,
|
|
385
|
-
lastFailure: void 0,
|
|
386
|
-
lastFailureAt: 0,
|
|
387
|
-
lastTool: void 0,
|
|
388
|
-
lastToolResult: void 0,
|
|
389
|
-
lastTurn: void 0,
|
|
390
|
-
pendingRecoveryAt: 0,
|
|
391
|
-
shortRun: 0,
|
|
392
|
-
lastShortAt: 0,
|
|
393
|
-
lastAssistantText: "",
|
|
394
|
-
sameTextRun: 0,
|
|
395
|
-
toolRun: void 0,
|
|
396
|
-
loopFired: false,
|
|
397
|
-
loopCancelled: false,
|
|
398
|
-
loopRetryTimer: void 0
|
|
399
|
-
});
|
|
400
|
-
var RECOVERY_WINDOW_MS = 10 * 60 * 1e3;
|
|
401
|
-
var ECHO_WINDOW_MS = 10 * 60 * 1e3;
|
|
402
|
-
function isOurEcho(state, sessionId, event) {
|
|
403
|
-
if (event.type !== "user/message") return false;
|
|
404
|
-
const message = event.data;
|
|
405
|
-
if (message.source.kind !== "user") return false;
|
|
406
|
-
const last = readLastSent(sessionId);
|
|
407
|
-
if (last.at === 0 || last.text === "") return false;
|
|
408
|
-
if (Date.now() - last.at > ECHO_WINDOW_MS) return false;
|
|
409
|
-
const text = message.content.filter((part) => part.type === "text").map((part) => part.text).join("");
|
|
410
|
-
return text === last.text;
|
|
411
|
-
}
|
|
412
|
-
async function pumpStream(open, onFrame, onReconnect, getBackoff, log, signal) {
|
|
413
|
-
let backoff = getBackoff();
|
|
414
|
-
while (!signal.aborted) {
|
|
415
|
-
let connected = false;
|
|
416
|
-
try {
|
|
417
|
-
for await (const envelope of open(signal)) {
|
|
418
|
-
connected = true;
|
|
419
|
-
onFrame(envelope.payload);
|
|
420
|
-
}
|
|
421
|
-
if (signal.aborted) return;
|
|
422
|
-
} catch (error) {
|
|
423
|
-
if (signal.aborted) return;
|
|
424
|
-
log(`stream error: ${error instanceof Error ? error.message : String(error)}`);
|
|
425
|
-
}
|
|
426
|
-
if (!connected) {
|
|
427
|
-
await sleep(backoff);
|
|
428
|
-
backoff = Math.min(backoff * 2, 15e3);
|
|
429
|
-
continue;
|
|
430
|
-
}
|
|
431
|
-
backoff = getBackoff();
|
|
432
|
-
onReconnect();
|
|
433
|
-
await sleep(backoff);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
var AutoContinueRunner = class {
|
|
437
|
-
/**
|
|
438
|
-
* @param api - shared wire client (ctx.connection.api).
|
|
439
|
-
* @param getConfig - read the current resolved configuration (settings scope).
|
|
440
|
-
*/
|
|
441
|
-
constructor(api, getConfig) {
|
|
442
|
-
this.api = api;
|
|
443
|
-
this.getConfig = getConfig;
|
|
444
|
-
this.states = /* @__PURE__ */ new Map();
|
|
445
|
-
this.muxAbort = new AbortController();
|
|
446
|
-
this.hostAbort = new AbortController();
|
|
447
|
-
this.disposed = false;
|
|
448
|
-
this.reconnectScans = 0;
|
|
449
|
-
/** 会话标题缓存(来自 session.list 投影, {sessionTitle} 占位符用)。 */
|
|
450
|
-
this.titles = /* @__PURE__ */ new Map();
|
|
451
|
-
const config = this.getConfig();
|
|
452
|
-
void this.runMux();
|
|
453
|
-
void this.runHost();
|
|
454
|
-
if (config.scanOnBoot) {
|
|
455
|
-
void this.bootScanLoop();
|
|
456
|
-
}
|
|
457
|
-
this.log(
|
|
458
|
-
`已启动(文本="${config.continueText}", 宽限 ${config.graceMs}ms, 冷却 ${config.cooldownMs}ms, 最多连续 ${config.maxConsecutive} 次)`
|
|
459
|
-
);
|
|
460
|
-
}
|
|
461
|
-
log(message) {
|
|
462
|
-
if (this.getConfig().verbose) console.info(`[auto-continue] ${message}`);
|
|
463
|
-
}
|
|
464
|
-
dispose() {
|
|
465
|
-
this.disposed = true;
|
|
466
|
-
this.muxAbort.abort();
|
|
467
|
-
this.hostAbort.abort();
|
|
468
|
-
for (const state of this.states.values()) {
|
|
469
|
-
if (state.pendingTimer !== void 0) clearTimeout(state.pendingTimer);
|
|
470
|
-
if (state.loopRetryTimer !== void 0) clearTimeout(state.loopRetryTimer);
|
|
471
|
-
}
|
|
472
|
-
this.states.clear();
|
|
473
|
-
}
|
|
474
|
-
state(sessionId) {
|
|
475
|
-
let state = this.states.get(sessionId);
|
|
476
|
-
if (state === void 0) {
|
|
477
|
-
state = freshState();
|
|
478
|
-
this.states.set(sessionId, state);
|
|
479
|
-
}
|
|
480
|
-
return state;
|
|
481
|
-
}
|
|
482
|
-
runMux() {
|
|
483
|
-
return pumpStream(
|
|
484
|
-
(signal) => this.api.events.mux({}, signal),
|
|
485
|
-
(payload) => this.onMuxFrame(payload),
|
|
486
|
-
() => this.scheduleReconnectScan(),
|
|
487
|
-
() => this.getConfig().reconnectBackoffMs,
|
|
488
|
-
(m) => this.log(m),
|
|
489
|
-
this.muxAbort.signal
|
|
490
|
-
);
|
|
491
|
-
}
|
|
492
|
-
runHost() {
|
|
493
|
-
return pumpStream(
|
|
494
|
-
(signal) => this.api.events.host({}, signal),
|
|
495
|
-
(payload) => this.onHostFrame(payload),
|
|
496
|
-
() => this.scheduleReconnectScan(),
|
|
497
|
-
() => this.getConfig().reconnectBackoffMs,
|
|
498
|
-
(m) => this.log(m),
|
|
499
|
-
this.hostAbort.signal
|
|
500
|
-
);
|
|
501
|
-
}
|
|
502
|
-
// ---------- mux 帧 ----------
|
|
503
|
-
onMuxFrame(frame) {
|
|
504
|
-
switch (frame.type) {
|
|
505
|
-
case "session/event":
|
|
506
|
-
if (frame.event.type === "tool/call") {
|
|
507
|
-
const name = frame.event.data.name;
|
|
508
|
-
if (typeof name === "string") {
|
|
509
|
-
const state = this.state(frame.sessionId);
|
|
510
|
-
state.lastTool = name;
|
|
511
|
-
state.lastToolResult = "pending";
|
|
512
|
-
state.shortRun = 0;
|
|
513
|
-
const key = `${name}
|
|
514
|
-
${frame.event.data.arguments}`;
|
|
515
|
-
if (state.toolRun?.key === key) {
|
|
516
|
-
state.toolRun.waiting = true;
|
|
517
|
-
} else {
|
|
518
|
-
state.toolRun = { key, count: 1, lastResult: void 0, waiting: false };
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
} else if (frame.event.type === "tool/result") {
|
|
522
|
-
const state = this.state(frame.sessionId);
|
|
523
|
-
if (state.lastToolResult === "pending") {
|
|
524
|
-
const facts = toolResultFacts(frame.event.data);
|
|
525
|
-
state.lastToolResult = facts;
|
|
526
|
-
const run = state.toolRun;
|
|
527
|
-
if (run !== void 0 && run.waiting) {
|
|
528
|
-
run.waiting = false;
|
|
529
|
-
if (run.lastResult !== void 0 && run.lastResult === facts.excerpt) {
|
|
530
|
-
run.count += 1;
|
|
531
|
-
this.checkLoop(frame.sessionId, state);
|
|
532
|
-
} else {
|
|
533
|
-
run.lastResult = facts.excerpt;
|
|
534
|
-
run.count = 1;
|
|
535
|
-
}
|
|
536
|
-
} else if (run !== void 0 && !run.waiting) {
|
|
537
|
-
run.lastResult = facts.excerpt;
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
} else if (frame.event.type === "assistant/message") {
|
|
541
|
-
const state = this.state(frame.sessionId);
|
|
542
|
-
this.onAssistantMessage(frame.sessionId, state, frame.event);
|
|
543
|
-
}
|
|
544
|
-
this.onSessionEvent(frame.sessionId, frame.event);
|
|
545
|
-
break;
|
|
546
|
-
case "session/queue":
|
|
547
|
-
this.state(frame.sessionId).queued = frame.items.length;
|
|
548
|
-
if (frame.items.length > 0) this.cancelPending(frame.sessionId, "出现排队消息");
|
|
549
|
-
break;
|
|
550
|
-
case "stream/error":
|
|
551
|
-
this.log(`mux stream/error: ${frame.error.code} ${frame.error.message}`);
|
|
552
|
-
break;
|
|
553
|
-
default:
|
|
554
|
-
break;
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
/** 从 assistant/message 事件提取纯文本。 */
|
|
558
|
-
assistantText(event) {
|
|
559
|
-
const content = event.data.message.content;
|
|
560
|
-
if (!Array.isArray(content)) return "";
|
|
561
|
-
return content.filter((part) => part.type === "text").map((part) => part.text).join("");
|
|
562
|
-
}
|
|
563
|
-
/**
|
|
564
|
-
* loop guard 信号 1(空转): 时间窗内连续短句且期间无工具调用。
|
|
565
|
-
* 短句 = 模型消息文本短于 loopShortChars; 长句、工具调用、或短句间隔超过
|
|
566
|
-
* loopWindowMs(正常思考的短文本散布在长时间里)都会重置计数。
|
|
567
|
-
*/
|
|
568
|
-
onAssistantMessage(sessionId, state, event) {
|
|
569
|
-
if (!this.getConfig().loopGuard) return;
|
|
570
|
-
const text = this.assistantText(event);
|
|
571
|
-
const trimmed = text.trim();
|
|
572
|
-
if (trimmed !== "" && trimmed === state.lastAssistantText) {
|
|
573
|
-
state.sameTextRun += 1;
|
|
574
|
-
} else {
|
|
575
|
-
state.lastAssistantText = trimmed;
|
|
576
|
-
state.sameTextRun = 1;
|
|
577
|
-
}
|
|
578
|
-
if (trimmed.length < this.getConfig().loopShortChars) {
|
|
579
|
-
const now = Date.now();
|
|
580
|
-
if (now - state.lastShortAt > this.getConfig().loopWindowMs) {
|
|
581
|
-
state.shortRun = 0;
|
|
582
|
-
}
|
|
583
|
-
state.shortRun += 1;
|
|
584
|
-
state.lastShortAt = now;
|
|
585
|
-
} else {
|
|
586
|
-
state.shortRun = 0;
|
|
587
|
-
state.lastShortAt = 0;
|
|
588
|
-
}
|
|
589
|
-
this.checkLoop(sessionId, state);
|
|
590
|
-
}
|
|
591
|
-
/** 两个循环信号的公共检查; 命中且本回合未打断过则打断。 */
|
|
592
|
-
checkLoop(sessionId, state) {
|
|
593
|
-
if (!this.getConfig().loopGuard) return;
|
|
594
|
-
if (state.loopFired) return;
|
|
595
|
-
if (!state.running) return;
|
|
596
|
-
const config = this.getConfig();
|
|
597
|
-
if (state.sameTextRun >= config.loopRepeatText) {
|
|
598
|
-
this.log(`检测到空转循环 ${sessionId}: 连续 ${state.sameTextRun} 条相同消息`);
|
|
599
|
-
void this.interruptLoop(sessionId, state);
|
|
600
|
-
} else if (state.shortRun >= config.loopShortCount) {
|
|
601
|
-
this.log(`检测到空转循环 ${sessionId}: 连续 ${state.shortRun} 条短句且无工具调用`);
|
|
602
|
-
void this.interruptLoop(sessionId, state);
|
|
603
|
-
} else if (state.toolRun !== void 0 && state.toolRun.count >= config.loopToolRepeat) {
|
|
604
|
-
const toolName = state.toolRun.key.split("\n")[0] ?? "?";
|
|
605
|
-
this.log(`检测到工具死循环 ${sessionId}: 「${toolName}」连续 ${state.toolRun.count} 次(同参数同结果)`);
|
|
606
|
-
void this.interruptLoop(sessionId, state);
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
/**
|
|
610
|
-
* 打断运行中的回合: cancel(带来源标记)+ 进冷却。
|
|
611
|
-
* 随后的 turn/end aborted 会因 loopCancelled 走「可恢复中断」路径,
|
|
612
|
-
* 用 loopText 重启回合——不会与用户手动停止混淆。
|
|
613
|
-
*/
|
|
614
|
-
async interruptLoop(sessionId, state) {
|
|
615
|
-
if (state.loopFired) return;
|
|
616
|
-
if (Date.now() - state.lastAttemptAt < this.cooldownFor(state)) {
|
|
617
|
-
this.log(`跳过循环打断 ${sessionId}: 处于冷却期`);
|
|
618
|
-
return;
|
|
619
|
-
}
|
|
620
|
-
state.loopFired = true;
|
|
621
|
-
state.loopCancelled = true;
|
|
622
|
-
state.lastAttemptAt = Date.now();
|
|
623
|
-
bumpStat({ looped: 1 });
|
|
624
|
-
try {
|
|
625
|
-
const response = await this.api.sessions.cancel({ sessionId });
|
|
626
|
-
this.log(
|
|
627
|
-
`已打断循环 ${sessionId}: ${response.result.ok ? "cancel 已受理" : "cancel 被拒绝"}`
|
|
628
|
-
);
|
|
629
|
-
} catch (error) {
|
|
630
|
-
this.log(`打断循环失败 ${sessionId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
631
|
-
state.loopCancelled = false;
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
onSessionEvent(sessionId, event) {
|
|
635
|
-
const state = this.state(sessionId);
|
|
636
|
-
switch (event.type) {
|
|
637
|
-
case "turn/start":
|
|
638
|
-
state.running = true;
|
|
639
|
-
state.lastTool = void 0;
|
|
640
|
-
state.lastToolResult = void 0;
|
|
641
|
-
state.shortRun = 0;
|
|
642
|
-
state.lastShortAt = 0;
|
|
643
|
-
state.lastAssistantText = "";
|
|
644
|
-
state.sameTextRun = 0;
|
|
645
|
-
state.toolRun = void 0;
|
|
646
|
-
state.loopFired = false;
|
|
647
|
-
state.loopCancelled = false;
|
|
648
|
-
if (state.loopRetryTimer !== void 0) {
|
|
649
|
-
clearTimeout(state.loopRetryTimer);
|
|
650
|
-
state.loopRetryTimer = void 0;
|
|
651
|
-
}
|
|
652
|
-
this.cancelPending(sessionId, "宿主自行开启新回合");
|
|
653
|
-
break;
|
|
654
|
-
case "turn/end": {
|
|
655
|
-
state.running = false;
|
|
656
|
-
this.cancelPending(sessionId, "收到新的 turn/end");
|
|
657
|
-
const reason = event.data.reason;
|
|
658
|
-
if (reason.kind === "completed") {
|
|
659
|
-
state.consecutive = 0;
|
|
660
|
-
state.lastFailure = void 0;
|
|
661
|
-
clearSendCount(sessionId);
|
|
662
|
-
this.noteRecovery(sessionId, "completed");
|
|
663
|
-
} else if (reason.kind === "aborted") {
|
|
664
|
-
if (state.loopCancelled) {
|
|
665
|
-
state.loopCancelled = false;
|
|
666
|
-
state.loopFired = false;
|
|
667
|
-
state.pendingRecoveryAt = 0;
|
|
668
|
-
state.shortRun = 0;
|
|
669
|
-
state.lastShortAt = 0;
|
|
670
|
-
state.lastAssistantText = "";
|
|
671
|
-
state.sameTextRun = 0;
|
|
672
|
-
state.toolRun = void 0;
|
|
673
|
-
const cooldown = this.cooldownFor(state);
|
|
674
|
-
const remaining = cooldown - (Date.now() - state.lastAttemptAt);
|
|
675
|
-
if (remaining > 0) {
|
|
676
|
-
if (state.loopRetryTimer !== void 0) clearTimeout(state.loopRetryTimer);
|
|
677
|
-
state.loopRetryTimer = setTimeout(() => {
|
|
678
|
-
state.loopRetryTimer = void 0;
|
|
679
|
-
this.schedule(sessionId, "loop:aborted");
|
|
680
|
-
}, remaining);
|
|
681
|
-
this.log(`loop 重启延迟 ${remaining}ms(冷却期) ${sessionId}`);
|
|
682
|
-
} else {
|
|
683
|
-
this.schedule(sessionId, "loop:aborted");
|
|
684
|
-
}
|
|
685
|
-
} else {
|
|
686
|
-
state.consecutive = 0;
|
|
687
|
-
state.pendingRecoveryAt = 0;
|
|
688
|
-
clearSendCount(sessionId);
|
|
689
|
-
}
|
|
690
|
-
} else if (reason.kind === "blocked") {
|
|
691
|
-
} else if (reason.kind === "interrupted") {
|
|
692
|
-
state.consecutive = 0;
|
|
693
|
-
state.pendingRecoveryAt = 0;
|
|
694
|
-
} else if (reason.kind === "error") {
|
|
695
|
-
const error = reason.error;
|
|
696
|
-
state.lastFailure = {
|
|
697
|
-
code: typeof error.code === "string" ? error.code : "UNKNOWN",
|
|
698
|
-
message: typeof error.message === "string" ? error.message : String(error),
|
|
699
|
-
...typeof error.status === "number" ? { status: error.status } : {}
|
|
700
|
-
};
|
|
701
|
-
state.lastTurn = event.data.turn;
|
|
702
|
-
state.lastFailureAt = Date.now();
|
|
703
|
-
this.noteRecovery(sessionId, "error");
|
|
704
|
-
this.onTurnFailure(sessionId, "turn/end:error", state.lastFailure);
|
|
705
|
-
} else if (reason.kind === "max-tokens") {
|
|
706
|
-
state.lastFailureAt = Date.now();
|
|
707
|
-
this.noteRecovery(sessionId, "error");
|
|
708
|
-
this.schedule(sessionId, "turn/end:max-tokens");
|
|
709
|
-
}
|
|
710
|
-
break;
|
|
711
|
-
}
|
|
712
|
-
case "user/message":
|
|
713
|
-
if (isOurEcho(state, sessionId, event)) break;
|
|
714
|
-
if (event.data.source.kind === "user") {
|
|
715
|
-
state.consecutive = 0;
|
|
716
|
-
clearSendCount(sessionId);
|
|
717
|
-
this.cancelPending(sessionId, "用户手动发送消息");
|
|
718
|
-
}
|
|
719
|
-
break;
|
|
720
|
-
default:
|
|
721
|
-
break;
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
// ---------- host 帧 ----------
|
|
725
|
-
onHostFrame(frame) {
|
|
726
|
-
switch (frame.type) {
|
|
727
|
-
case "host/session-status":
|
|
728
|
-
this.state(frame.sessionId).running = frame.running;
|
|
729
|
-
if (frame.running) this.cancelPending(frame.sessionId, "宿主报告会话开始运行");
|
|
730
|
-
break;
|
|
731
|
-
case "host/session-added":
|
|
732
|
-
this.state(frame.sessionId).subagent = frame.parentSessionId !== void 0;
|
|
733
|
-
break;
|
|
734
|
-
case "host/agent-error":
|
|
735
|
-
if (this.state(frame.sessionId).subagent) break;
|
|
736
|
-
this.log(`host/agent-error(${frame.sessionId}): ${frame.message}`);
|
|
737
|
-
if (!isTransientAgentError(frame.message)) {
|
|
738
|
-
this.log(`跳过 ${frame.sessionId}: 永久性 agent 错误 — ${frame.message}`);
|
|
739
|
-
bumpStat({ skipped: 1 });
|
|
740
|
-
if (this.getConfig().notify) {
|
|
741
|
-
notify(
|
|
742
|
-
"dsh-auto-continue: 未自动继续",
|
|
743
|
-
`${frame.sessionId}: 永久性 agent 错误 ${frame.message.slice(0, 120)}`,
|
|
744
|
-
this.notifyOptions(frame.sessionId)
|
|
745
|
-
);
|
|
746
|
-
}
|
|
747
|
-
break;
|
|
748
|
-
}
|
|
749
|
-
this.schedule(frame.sessionId, "host/agent-error");
|
|
750
|
-
break;
|
|
751
|
-
case "host/session-removed":
|
|
752
|
-
this.cancelPending(frame.sessionId, "会话已移除");
|
|
753
|
-
this.states.delete(frame.sessionId);
|
|
754
|
-
break;
|
|
755
|
-
default:
|
|
756
|
-
break;
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
// ---------- 调度 ----------
|
|
760
|
-
/** 回合失败入口: 先做错误分类, 永久性失败跳过并通知, 临时性失败走正常调度。 */
|
|
761
|
-
onTurnFailure(sessionId, reason, failure) {
|
|
762
|
-
const config = this.getConfig();
|
|
763
|
-
if (config.classify && !isTransientFailure(failure)) {
|
|
764
|
-
const summary = `${failure.code}${failure.status !== void 0 ? ` (HTTP ${failure.status})` : ""}`;
|
|
765
|
-
this.log(`跳过 ${sessionId}(${reason}): 永久性失败 ${summary} — ${failure.message}`);
|
|
766
|
-
bumpStat({ skipped: 1, code: failure.code });
|
|
767
|
-
if (config.notify) {
|
|
768
|
-
notify(
|
|
769
|
-
"dsh-auto-continue: 未自动继续",
|
|
770
|
-
`${sessionId}: 永久性错误 ${summary},需要人工处理`,
|
|
771
|
-
this.notifyOptions(sessionId)
|
|
772
|
-
);
|
|
773
|
-
}
|
|
774
|
-
return;
|
|
775
|
-
}
|
|
776
|
-
this.schedule(sessionId, reason);
|
|
777
|
-
}
|
|
778
|
-
/** 通知操作按钮与回调(「立即续跑」/「暂停该会话 1 小时」)。 */
|
|
779
|
-
notifyOptions(sessionId) {
|
|
780
|
-
return {
|
|
781
|
-
actions: [
|
|
782
|
-
{ action: "resume", title: "立即续跑" },
|
|
783
|
-
{ action: "pause1h", title: "暂停该会话 1 小时" }
|
|
784
|
-
],
|
|
785
|
-
onAction: (action) => this.onNotifyAction(sessionId, action)
|
|
786
|
-
};
|
|
787
|
-
}
|
|
788
|
-
onNotifyAction(sessionId, action) {
|
|
789
|
-
if (action === "resume") {
|
|
790
|
-
this.log(`通知按钮: 立即续跑 ${sessionId}`);
|
|
791
|
-
void this.resumeNow(sessionId);
|
|
792
|
-
} else if (action === "pause1h") {
|
|
793
|
-
this.log(`通知按钮: 暂停 ${sessionId} 1 小时`);
|
|
794
|
-
pauseSession(sessionId, 60 * 60 * 1e3);
|
|
795
|
-
this.cancelPending(sessionId, "通知按钮暂停该会话");
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
/** 恢复结果记账: 自动发送后窗口内的回合结束, 判定恢复成功或失败。 */
|
|
799
|
-
noteRecovery(sessionId, outcome) {
|
|
800
|
-
const state = this.state(sessionId);
|
|
801
|
-
if (state.pendingRecoveryAt === 0) return;
|
|
802
|
-
if (Date.now() - state.pendingRecoveryAt > RECOVERY_WINDOW_MS) {
|
|
803
|
-
state.pendingRecoveryAt = 0;
|
|
804
|
-
return;
|
|
805
|
-
}
|
|
806
|
-
state.pendingRecoveryAt = 0;
|
|
807
|
-
bumpStat(outcome === "completed" ? { recovered: 1 } : { failed: 1 });
|
|
808
|
-
this.log(`恢复结果(${sessionId}): ${outcome === "completed" ? "成功" : "失败"}`);
|
|
809
|
-
}
|
|
810
|
-
/** 立即为该会话发送一次自动继续(无视冷却与连续上限; 由通知按钮触发)。 */
|
|
811
|
-
async resumeNow(sessionId) {
|
|
812
|
-
if (this.disposed) return;
|
|
813
|
-
const state = this.state(sessionId);
|
|
814
|
-
if (state.subagent) return;
|
|
815
|
-
if (state.pendingTimer !== void 0) {
|
|
816
|
-
clearTimeout(state.pendingTimer);
|
|
817
|
-
state.pendingTimer = void 0;
|
|
818
|
-
}
|
|
819
|
-
await this.fire(sessionId, "manual:notification", true);
|
|
820
|
-
}
|
|
821
|
-
/** 本会话当前生效的冷却间隔(自适应退避)。 */
|
|
822
|
-
cooldownFor(state) {
|
|
823
|
-
const config = this.getConfig();
|
|
824
|
-
return effectiveCooldown(
|
|
825
|
-
state.consecutive,
|
|
826
|
-
config.cooldownMs,
|
|
827
|
-
config.backoffFactor,
|
|
828
|
-
config.backoffMaxMs
|
|
829
|
-
);
|
|
830
|
-
}
|
|
831
|
-
schedule(sessionId, reason) {
|
|
832
|
-
const state = this.state(sessionId);
|
|
833
|
-
const config = this.getConfig();
|
|
834
|
-
if (state.subagent) return;
|
|
835
|
-
if (config.paused) {
|
|
836
|
-
this.log(`跳过 ${sessionId}(${reason}): 全局暂停中`);
|
|
837
|
-
return;
|
|
838
|
-
}
|
|
839
|
-
if (Date.now() < sessionPauseUntil(sessionId)) {
|
|
840
|
-
this.log(`跳过 ${sessionId}(${reason}): 会话暂停中`);
|
|
841
|
-
return;
|
|
842
|
-
}
|
|
843
|
-
if (state.pendingTimer !== void 0) return;
|
|
844
|
-
if (Date.now() - state.lastAttemptAt < this.cooldownFor(state)) return;
|
|
845
|
-
if (state.consecutive >= config.maxConsecutive) {
|
|
846
|
-
this.log(
|
|
847
|
-
`跳过 ${sessionId}(${reason}): 已连续自动继续 ${state.consecutive} 次, 等待用户介入或成功回合`
|
|
848
|
-
);
|
|
849
|
-
return;
|
|
850
|
-
}
|
|
851
|
-
if (state.queued > 0) return;
|
|
852
|
-
const timer = setTimeout(() => {
|
|
853
|
-
if (state.pendingTimer !== timer) return;
|
|
854
|
-
state.pendingTimer = void 0;
|
|
855
|
-
void this.fire(sessionId, reason);
|
|
856
|
-
}, config.graceMs);
|
|
857
|
-
state.pendingTimer = timer;
|
|
858
|
-
const template = reason.startsWith("loop:") ? config.loopText : reason.includes("max-tokens") ? config.continueTextMaxTokens : config.continueText;
|
|
859
|
-
this.log(
|
|
860
|
-
`检测到非人为中断 ${sessionId}(${reason}), ${config.graceMs}ms 后自动发送「${template}」`
|
|
861
|
-
);
|
|
862
|
-
}
|
|
863
|
-
cancelPending(sessionId, why) {
|
|
864
|
-
const state = this.state(sessionId);
|
|
865
|
-
if (state.pendingTimer === void 0) return;
|
|
866
|
-
clearTimeout(state.pendingTimer);
|
|
867
|
-
state.pendingTimer = void 0;
|
|
868
|
-
this.log(`取消 ${sessionId} 的自动继续(${why})`);
|
|
869
|
-
}
|
|
870
|
-
async fire(sessionId, reason, force = false) {
|
|
871
|
-
if (this.disposed) return;
|
|
872
|
-
const state = this.state(sessionId);
|
|
873
|
-
const config = this.getConfig();
|
|
874
|
-
if (state.running === void 0) {
|
|
875
|
-
const running = await this.runningViaList(sessionId);
|
|
876
|
-
if (running === void 0 || running) {
|
|
877
|
-
this.log(`跳过 ${sessionId}: 无法确认空闲(${running === void 0 ? "未知" : "运行中"})`);
|
|
878
|
-
return;
|
|
879
|
-
}
|
|
880
|
-
} else if (state.running) {
|
|
881
|
-
this.log(`跳过 ${sessionId}: 会话仍在运行`);
|
|
882
|
-
return;
|
|
883
|
-
}
|
|
884
|
-
if (state.queued > 0) {
|
|
885
|
-
this.log(`跳过 ${sessionId}: 已有排队消息`);
|
|
886
|
-
return;
|
|
887
|
-
}
|
|
888
|
-
if (!force && readSendCount(sessionId).count >= config.maxConsecutive) {
|
|
889
|
-
this.log(`跳过 ${sessionId}: 发送计数已达上限 ${config.maxConsecutive}, 等待用户介入或成功回合`);
|
|
890
|
-
return;
|
|
891
|
-
}
|
|
892
|
-
const template = reason.startsWith("loop:") ? config.loopText : reason.includes("max-tokens") ? config.continueTextMaxTokens : config.continueText;
|
|
893
|
-
let sessionTitle;
|
|
894
|
-
if (template.includes("{sessionTitle}")) {
|
|
895
|
-
sessionTitle = this.titles.get(sessionId);
|
|
896
|
-
if (sessionTitle === void 0) {
|
|
897
|
-
const info = await this.fetchSessionInfo(sessionId);
|
|
898
|
-
sessionTitle = info?.title;
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
const text = this.buildContinueText(config, state, template, sessionTitle);
|
|
902
|
-
const zone = clientTimeZone();
|
|
903
|
-
await withSendLock(sessionId, async () => {
|
|
904
|
-
if (this.disposed) return;
|
|
905
|
-
if (state.queued > 0) {
|
|
906
|
-
this.log(`跳过 ${sessionId}: 已有排队消息`);
|
|
907
|
-
return;
|
|
908
|
-
}
|
|
909
|
-
if (!force && Date.now() - readLastSend(sessionId) < this.cooldownFor(state)) {
|
|
910
|
-
this.log(`跳过 ${sessionId}: 其他标签页刚发送过`);
|
|
911
|
-
return;
|
|
912
|
-
}
|
|
913
|
-
if (!force && readSendCount(sessionId).count >= config.maxConsecutive) {
|
|
914
|
-
this.log(`跳过 ${sessionId}: 发送计数已达上限 ${config.maxConsecutive}, 等待用户介入或成功回合`);
|
|
915
|
-
return;
|
|
916
|
-
}
|
|
917
|
-
state.lastAttemptAt = Date.now();
|
|
918
|
-
try {
|
|
919
|
-
const response = await this.api.sessions.prompt({
|
|
920
|
-
sessionId,
|
|
921
|
-
mode: "queue",
|
|
922
|
-
content: [{ type: "text", text }],
|
|
923
|
-
...zone === void 0 ? {} : { clientTimeZone: zone }
|
|
924
|
-
});
|
|
925
|
-
if (response.result.ok) {
|
|
926
|
-
const now = Date.now();
|
|
927
|
-
state.consecutive += 1;
|
|
928
|
-
state.lastAutoAt = now;
|
|
929
|
-
state.lastSentText = text;
|
|
930
|
-
state.pendingRecoveryAt = now;
|
|
931
|
-
writeLastSend(sessionId, now, text);
|
|
932
|
-
bumpSendCount(sessionId);
|
|
933
|
-
bumpStat({ sent: 1, ...state.lastFailure !== void 0 ? { code: state.lastFailure.code } : {} });
|
|
934
|
-
this.log(`已自动发送「${text}」到 ${sessionId}(${reason}), 第 ${state.consecutive} 次连续`);
|
|
935
|
-
if (config.notify) {
|
|
936
|
-
notify(
|
|
937
|
-
"dsh-auto-continue: 已自动继续",
|
|
938
|
-
`${sessionId}: 已发送「${text}」(第 ${state.consecutive} 次连续)`,
|
|
939
|
-
this.notifyOptions(sessionId)
|
|
940
|
-
);
|
|
941
|
-
}
|
|
942
|
-
if (state.consecutive >= config.maxConsecutive) {
|
|
943
|
-
bumpStat({ gaveUp: 1 });
|
|
944
|
-
this.log(`达到连续上限 ${config.maxConsecutive} 次, 停止自动继续 ${sessionId}`);
|
|
945
|
-
if (config.notify) {
|
|
946
|
-
notify(
|
|
947
|
-
"dsh-auto-continue: 已停止自动继续",
|
|
948
|
-
`${sessionId}: 连续失败 ${state.consecutive} 次, 需要人工介入`,
|
|
949
|
-
this.notifyOptions(sessionId)
|
|
950
|
-
);
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
} else {
|
|
954
|
-
this.log(
|
|
955
|
-
`发送失败 ${sessionId}: ${response.result.error.code} ${response.result.error.message}`
|
|
956
|
-
);
|
|
957
|
-
}
|
|
958
|
-
} catch (error) {
|
|
959
|
-
this.log(`发送异常 ${sessionId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
960
|
-
}
|
|
961
|
-
});
|
|
962
|
-
}
|
|
963
|
-
/**
|
|
964
|
-
* 组装本次续跑消息: 模板填充 + 幂等护栏。
|
|
965
|
-
* 护栏依据上一步工具调用的执行状态附加指引, 防止重跑副作用操作:
|
|
966
|
-
* - 结果未确认(可能已部分执行)→ 提示先确认状态、不要重复执行
|
|
967
|
-
* - 已确认成功 → 提示已完成、不要重复执行
|
|
968
|
-
* - 已失败 → 不加护栏(重试工具本来就是目的)
|
|
969
|
-
*/
|
|
970
|
-
buildContinueText(config, state, template, sessionTitle) {
|
|
971
|
-
let text = fillTemplate(template, {
|
|
972
|
-
facts: state.lastFailure,
|
|
973
|
-
tool: state.lastTool,
|
|
974
|
-
turn: state.lastTurn,
|
|
975
|
-
errorCount: state.consecutive + 1,
|
|
976
|
-
sessionTitle,
|
|
977
|
-
elapsedMs: state.lastFailureAt > 0 ? Date.now() - state.lastFailureAt : void 0
|
|
978
|
-
});
|
|
979
|
-
if (!config.guardTools) return text;
|
|
980
|
-
const guard = this.currentGuard(state);
|
|
981
|
-
if (guard.kind === "pending") {
|
|
982
|
-
text += ` ${fillTemplate(config.guardPendingText, { tool: guard.tool, result: guard.result })}`;
|
|
983
|
-
} else if (guard.kind === "done") {
|
|
984
|
-
text += ` ${fillTemplate(config.guardDoneText, { tool: guard.tool, result: guard.result })}`;
|
|
985
|
-
}
|
|
986
|
-
return text;
|
|
987
|
-
}
|
|
988
|
-
/** 上一步工具调用的护栏状态(实时路径, 由 mux 帧维护)。 */
|
|
989
|
-
currentGuard(state) {
|
|
990
|
-
if (state.lastTool === void 0 || state.lastToolResult === void 0) return { kind: "none" };
|
|
991
|
-
if (state.lastToolResult === "pending") return { kind: "pending", tool: state.lastTool };
|
|
992
|
-
if (state.lastToolResult.ok) {
|
|
993
|
-
return { kind: "done", tool: state.lastTool, result: state.lastToolResult.excerpt };
|
|
994
|
-
}
|
|
995
|
-
return { kind: "failed", tool: state.lastTool };
|
|
996
|
-
}
|
|
997
|
-
/** 查一次 session.list, 顺带缓存该会话的标题。 */
|
|
998
|
-
async fetchSessionInfo(sessionId) {
|
|
999
|
-
try {
|
|
1000
|
-
const response = await this.api.sessions.list({});
|
|
1001
|
-
if (!response.result.ok) return void 0;
|
|
1002
|
-
const item = response.result.value.items.find(
|
|
1003
|
-
(summary) => summary.sessionId === sessionId
|
|
1004
|
-
);
|
|
1005
|
-
if (item === void 0) return void 0;
|
|
1006
|
-
const title = item.projections?.values?.title;
|
|
1007
|
-
if (typeof title === "string" && title !== "") this.titles.set(sessionId, title);
|
|
1008
|
-
return { running: item.running, title: typeof title === "string" ? title : void 0 };
|
|
1009
|
-
} catch {
|
|
1010
|
-
return void 0;
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
async runningViaList(sessionId) {
|
|
1014
|
-
const info = await this.fetchSessionInfo(sessionId);
|
|
1015
|
-
return info?.running;
|
|
1016
|
-
}
|
|
1017
|
-
// ---------- 启动/重连扫描 ----------
|
|
1018
|
-
scheduleReconnectScan() {
|
|
1019
|
-
this.reconnectScans += 1;
|
|
1020
|
-
const scan = this.reconnectScans;
|
|
1021
|
-
setTimeout(() => {
|
|
1022
|
-
if (scan !== this.reconnectScans || this.disposed) return;
|
|
1023
|
-
void this.scanLoop(6, this.getConfig().reconnectScanDelayMs);
|
|
1024
|
-
}, this.getConfig().reconnectScanDelayMs);
|
|
1025
|
-
}
|
|
1026
|
-
async bootScanLoop() {
|
|
1027
|
-
await this.scanLoop(Infinity, 3e3);
|
|
1028
|
-
}
|
|
1029
|
-
/** 反复尝试扫描, 直到成功(宿主就绪)或达到次数上限。 */
|
|
1030
|
-
async scanLoop(attempts, delayMs) {
|
|
1031
|
-
for (let attempt = 0; attempt < attempts && !this.disposed; attempt += 1) {
|
|
1032
|
-
try {
|
|
1033
|
-
if (await this.scanInterrupted()) return;
|
|
1034
|
-
} catch (error) {
|
|
1035
|
-
if (this.disposed) return;
|
|
1036
|
-
if (attempt % 10 === 0) {
|
|
1037
|
-
this.log(
|
|
1038
|
-
`扫描失败(${attempt + 1}/${attempts === Infinity ? "∞" : attempts}): ${error instanceof Error ? error.message : String(error)}`
|
|
1039
|
-
);
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
if (attempt + 1 < attempts) await sleep(delayMs);
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
/**
|
|
1046
|
-
* 扫描最近中断过的会话: 最后回合以非人为原因结束, 且其后没有新回合或用户消息。
|
|
1047
|
-
* @returns 是否成功完成一次扫描(宿主就绪)。
|
|
1048
|
-
*/
|
|
1049
|
-
async scanInterrupted() {
|
|
1050
|
-
const config = this.getConfig();
|
|
1051
|
-
if (config.paused) return true;
|
|
1052
|
-
const response = await this.api.sessions.list({});
|
|
1053
|
-
if (!response.result.ok) return false;
|
|
1054
|
-
const items = response.result.value.items;
|
|
1055
|
-
for (const summary of items) {
|
|
1056
|
-
const title = summary.projections?.values?.title;
|
|
1057
|
-
if (typeof title === "string" && title !== "") this.titles.set(summary.sessionId, title);
|
|
1058
|
-
}
|
|
1059
|
-
const candidates = items.filter((summary) => !summary.running && summary.parentSessionId === void 0).slice(0, config.scanLimit);
|
|
1060
|
-
const now = Date.now();
|
|
1061
|
-
for (const summary of candidates) {
|
|
1062
|
-
if (this.disposed) return true;
|
|
1063
|
-
const state = this.state(summary.sessionId);
|
|
1064
|
-
if (state.pendingTimer !== void 0) continue;
|
|
1065
|
-
if (state.consecutive >= config.maxConsecutive) continue;
|
|
1066
|
-
if (now - state.lastAttemptAt < this.cooldownFor(state)) continue;
|
|
1067
|
-
if (now < sessionPauseUntil(summary.sessionId)) continue;
|
|
1068
|
-
let events;
|
|
1069
|
-
try {
|
|
1070
|
-
const page = await this.api.sessions.history({
|
|
1071
|
-
sessionId: summary.sessionId,
|
|
1072
|
-
maxMessages: 30
|
|
1073
|
-
});
|
|
1074
|
-
if (!page.result.ok) continue;
|
|
1075
|
-
events = page.result.value.events;
|
|
1076
|
-
} catch {
|
|
1077
|
-
continue;
|
|
1078
|
-
}
|
|
1079
|
-
let lastEnd;
|
|
1080
|
-
for (let i = events.length - 1; i >= 0; i -= 1) {
|
|
1081
|
-
const event = events[i]?.event;
|
|
1082
|
-
if (event !== void 0 && event.type === "turn/end") {
|
|
1083
|
-
lastEnd = event;
|
|
1084
|
-
break;
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
if (lastEnd === void 0) continue;
|
|
1088
|
-
const reason = lastEnd.data.reason;
|
|
1089
|
-
if (!isNonHumanReason(reason.kind)) continue;
|
|
1090
|
-
if (lastEnd.time < now - config.freshMs) continue;
|
|
1091
|
-
let superseded = false;
|
|
1092
|
-
for (const entry of events) {
|
|
1093
|
-
const event = entry.event;
|
|
1094
|
-
if (event.seq <= lastEnd.seq) continue;
|
|
1095
|
-
if (event.type === "turn/start") superseded = true;
|
|
1096
|
-
if (event.type === "user/message" && event.data.source.kind === "user") superseded = true;
|
|
1097
|
-
if (superseded) break;
|
|
1098
|
-
}
|
|
1099
|
-
if (superseded) continue;
|
|
1100
|
-
this.applyGuardFromEvents(state, events, lastEnd.seq);
|
|
1101
|
-
this.log(`扫描发现中断 ${summary.sessionId}(turn/end:${reason.kind}), 安排自动继续`);
|
|
1102
|
-
this.schedule(summary.sessionId, `scan:turn/end:${reason.kind}`);
|
|
1103
|
-
}
|
|
1104
|
-
return true;
|
|
1105
|
-
}
|
|
1106
|
-
/** 从历史事件恢复上一步工具调用状态(扫描路径的幂等护栏)。 */
|
|
1107
|
-
applyGuardFromEvents(state, events, untilSeq) {
|
|
1108
|
-
state.lastTool = void 0;
|
|
1109
|
-
state.lastToolResult = void 0;
|
|
1110
|
-
let call;
|
|
1111
|
-
for (const entry of events) {
|
|
1112
|
-
const event = entry.event;
|
|
1113
|
-
if (event.seq >= untilSeq) continue;
|
|
1114
|
-
if (event.type === "tool/call") call = event;
|
|
1115
|
-
}
|
|
1116
|
-
if (call === void 0) return;
|
|
1117
|
-
state.lastTool = call.data.name;
|
|
1118
|
-
state.lastToolResult = "pending";
|
|
1119
|
-
for (const entry of events) {
|
|
1120
|
-
const event = entry.event;
|
|
1121
|
-
if (event.seq <= call.seq || event.seq >= untilSeq) continue;
|
|
1122
|
-
if (event.type === "tool/result") {
|
|
1123
|
-
state.lastToolResult = toolResultFacts(event.data);
|
|
1124
|
-
break;
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
};
|
|
1129
|
-
|
|
1130
38
|
// src/client/locales.ts
|
|
1131
39
|
var zh = {
|
|
1132
40
|
"card.title": "自动继续",
|
|
@@ -1303,6 +211,170 @@ var en = {
|
|
|
1303
211
|
var import_react = require("react");
|
|
1304
212
|
var import_client = require("@deepseek-ai/dsh-client-runtime/client");
|
|
1305
213
|
|
|
214
|
+
// src/client/engine.ts
|
|
215
|
+
var DEFAULT_CONFIG = {
|
|
216
|
+
continueText: "继续",
|
|
217
|
+
continueTextMaxTokens: "继续",
|
|
218
|
+
guardTools: true,
|
|
219
|
+
guardPendingText: "(上一步工具「{tool}」可能未完成, 先确认状态再继续, 不要重复执行)",
|
|
220
|
+
guardDoneText: "(上一步工具「{tool}」已完成, 结果: {result}; 不要重复执行, 直接继续)",
|
|
221
|
+
graceMs: 3e3,
|
|
222
|
+
cooldownMs: 2e4,
|
|
223
|
+
maxConsecutive: 3,
|
|
224
|
+
scanOnBoot: true,
|
|
225
|
+
scanLimit: 8,
|
|
226
|
+
freshMs: 15 * 60 * 1e3,
|
|
227
|
+
reconnectScanDelayMs: 5e3,
|
|
228
|
+
reconnectBackoffMs: 3e3,
|
|
229
|
+
verbose: true,
|
|
230
|
+
classify: true,
|
|
231
|
+
backoffFactor: 2,
|
|
232
|
+
backoffMaxMs: 3e5,
|
|
233
|
+
notify: false,
|
|
234
|
+
paused: false,
|
|
235
|
+
loopGuard: true,
|
|
236
|
+
loopShortChars: 40,
|
|
237
|
+
loopWindowMs: 3e4,
|
|
238
|
+
loopShortCount: 12,
|
|
239
|
+
loopRepeatText: 4,
|
|
240
|
+
loopToolRepeat: 5,
|
|
241
|
+
loopText: "(检测到你可能陷入循环, 请停止重复刚才的动作, 换一种方式继续)"
|
|
242
|
+
};
|
|
243
|
+
var lockPrefix = "dsh-auto-continue:";
|
|
244
|
+
var SEND_COUNT_WINDOW_MS = 10 * 60 * 1e3;
|
|
245
|
+
var statsKey = `${lockPrefix}stats`;
|
|
246
|
+
var RECOVERY_WINDOW_MS = 10 * 60 * 1e3;
|
|
247
|
+
var ECHO_WINDOW_MS = 10 * 60 * 1e3;
|
|
248
|
+
|
|
249
|
+
// src/client/bridge.ts
|
|
250
|
+
var EMPTY_STATS = {
|
|
251
|
+
date: "",
|
|
252
|
+
sent: 0,
|
|
253
|
+
skipped: 0,
|
|
254
|
+
recovered: 0,
|
|
255
|
+
failed: 0,
|
|
256
|
+
gaveUp: 0,
|
|
257
|
+
looped: 0,
|
|
258
|
+
byCode: {}
|
|
259
|
+
};
|
|
260
|
+
var state = { stats: EMPTY_STATS, paused: [] };
|
|
261
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
262
|
+
function pausedSessions() {
|
|
263
|
+
return state.paused;
|
|
264
|
+
}
|
|
265
|
+
function readTodayStats() {
|
|
266
|
+
return state.stats;
|
|
267
|
+
}
|
|
268
|
+
function resetTodayStats() {
|
|
269
|
+
void postAction({ action: "reset-stats" });
|
|
270
|
+
}
|
|
271
|
+
function unpauseSession(sessionId) {
|
|
272
|
+
void postAction({ action: "unpause", sessionId });
|
|
273
|
+
}
|
|
274
|
+
function subscribeBridge(listener) {
|
|
275
|
+
listeners.add(listener);
|
|
276
|
+
return () => {
|
|
277
|
+
listeners.delete(listener);
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
async function postAction(payload) {
|
|
281
|
+
try {
|
|
282
|
+
await fetch("/api/auto-continue-action", {
|
|
283
|
+
method: "POST",
|
|
284
|
+
headers: { "content-type": "application/json" },
|
|
285
|
+
body: JSON.stringify(payload)
|
|
286
|
+
});
|
|
287
|
+
} catch {
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function handleEvent(event) {
|
|
291
|
+
if (event.type === "state") {
|
|
292
|
+
state = {
|
|
293
|
+
stats: event.stats ?? EMPTY_STATS,
|
|
294
|
+
paused: event.paused ?? []
|
|
295
|
+
};
|
|
296
|
+
for (const listener of listeners) listener();
|
|
297
|
+
} else if (event.type === "notice" && event.notice !== void 0) {
|
|
298
|
+
showNotification(event.notice);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function showNotification(notice) {
|
|
302
|
+
try {
|
|
303
|
+
const N = globalThis.Notification;
|
|
304
|
+
if (typeof N === "undefined") return;
|
|
305
|
+
const permission = N.permission;
|
|
306
|
+
const create = () => {
|
|
307
|
+
const instance = new N(notice.title, {
|
|
308
|
+
body: notice.body,
|
|
309
|
+
...notice.actions.length > 0 ? { actions: notice.actions } : {}
|
|
310
|
+
});
|
|
311
|
+
const target = instance;
|
|
312
|
+
target.onclick = () => {
|
|
313
|
+
try {
|
|
314
|
+
globalThis.focus?.();
|
|
315
|
+
} catch {
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
target.onaction = (event) => {
|
|
319
|
+
if (notice.sessionId !== void 0) {
|
|
320
|
+
void postAction({ action: event.action, sessionId: notice.sessionId });
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
};
|
|
324
|
+
if (permission === "granted") {
|
|
325
|
+
create();
|
|
326
|
+
} else if (permission === "default") {
|
|
327
|
+
void N.requestPermission?.().then((result) => {
|
|
328
|
+
if (result === "granted") create();
|
|
329
|
+
}).catch(() => {
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
} catch {
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function startBridge() {
|
|
336
|
+
let stopped = false;
|
|
337
|
+
let controller;
|
|
338
|
+
const loop = async () => {
|
|
339
|
+
while (!stopped) {
|
|
340
|
+
controller = new AbortController();
|
|
341
|
+
try {
|
|
342
|
+
const response = await fetch("/api/auto-continue-bridge", { signal: controller.signal });
|
|
343
|
+
if (!response.ok || response.body === null) throw new Error(`bridge HTTP ${response.status}`);
|
|
344
|
+
const reader = response.body.getReader();
|
|
345
|
+
const decoder = new TextDecoder();
|
|
346
|
+
let buffer = "";
|
|
347
|
+
for (; ; ) {
|
|
348
|
+
const { done, value } = await reader.read();
|
|
349
|
+
if (done) break;
|
|
350
|
+
buffer += decoder.decode(value, { stream: true });
|
|
351
|
+
let idx = buffer.indexOf("\n\n");
|
|
352
|
+
while (idx !== -1) {
|
|
353
|
+
const chunk = buffer.slice(0, idx);
|
|
354
|
+
buffer = buffer.slice(idx + 2);
|
|
355
|
+
for (const line of chunk.split("\n")) {
|
|
356
|
+
if (line.startsWith("data: ")) {
|
|
357
|
+
try {
|
|
358
|
+
handleEvent(JSON.parse(line.slice(6)));
|
|
359
|
+
} catch {
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
idx = buffer.indexOf("\n\n");
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
} catch {
|
|
367
|
+
}
|
|
368
|
+
if (!stopped) await new Promise((resolve) => setTimeout(resolve, 3e3));
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
void loop();
|
|
372
|
+
return () => {
|
|
373
|
+
stopped = true;
|
|
374
|
+
controller?.abort();
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
1306
378
|
// src/client/settings-form.ts
|
|
1307
379
|
function numberField(field, min = 0) {
|
|
1308
380
|
return {
|
|
@@ -1731,10 +803,10 @@ var AutoContinueSettingsCardController = class {
|
|
|
1731
803
|
};
|
|
1732
804
|
function SettingsCard(props) {
|
|
1733
805
|
const [open, setOpen] = (0, import_react.useState)(false);
|
|
1734
|
-
const { state } = props;
|
|
1735
|
-
if (!
|
|
806
|
+
const { state: state2 } = props;
|
|
807
|
+
if (!state2.available) return null;
|
|
1736
808
|
const title = props.t(props.titleKey);
|
|
1737
|
-
const blocked = !
|
|
809
|
+
const blocked = !state2.dirty || state2.invalid || state2.saving;
|
|
1738
810
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { className: open ? "dshAcCard dshAcCardOpen" : "dshAcCard", children: [
|
|
1739
811
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1740
812
|
"button",
|
|
@@ -1750,27 +822,27 @@ function SettingsCard(props) {
|
|
|
1750
822
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dshAcName", children: title }),
|
|
1751
823
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dshAcDescription", children: props.t(props.descriptionKey) })
|
|
1752
824
|
] }),
|
|
1753
|
-
|
|
825
|
+
state2.dirty ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dshAcPending", title: props.t("chrome.unsaved"), children: props.t("chrome.unsaved") }) : null,
|
|
1754
826
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: open ? "dshAcChevron dshAcChevronOpen" : "dshAcChevron", children: "▾" })
|
|
1755
827
|
]
|
|
1756
828
|
}
|
|
1757
829
|
),
|
|
1758
830
|
open ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshAcBody", children: [
|
|
1759
|
-
!
|
|
831
|
+
!state2.writable ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dshAcReadOnly", role: "status", children: props.t("chrome.readOnly") }) : null,
|
|
1760
832
|
props.children,
|
|
1761
833
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dshAcFooter", children: [
|
|
1762
|
-
|
|
834
|
+
state2.failed ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dshAcFailed", role: "status", children: props.t("chrome.saveFailed") }) : null,
|
|
1763
835
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1764
836
|
"button",
|
|
1765
837
|
{
|
|
1766
838
|
type: "button",
|
|
1767
839
|
className: "dshAcDiscard",
|
|
1768
|
-
disabled: !
|
|
840
|
+
disabled: !state2.dirty || state2.saving,
|
|
1769
841
|
onClick: props.onDiscard,
|
|
1770
842
|
children: props.t("chrome.discard")
|
|
1771
843
|
}
|
|
1772
844
|
),
|
|
1773
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "dshAcSave", disabled: blocked, onClick: props.onSave, children: props.t(!
|
|
845
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "dshAcSave", disabled: blocked, onClick: props.onSave, children: props.t(!state2.saving ? "chrome.save" : "chrome.saving") })
|
|
1774
846
|
] })
|
|
1775
847
|
] }) : null
|
|
1776
848
|
] });
|
|
@@ -1832,8 +904,12 @@ function LivePanels(props) {
|
|
|
1832
904
|
const { t } = props;
|
|
1833
905
|
const [, refresh] = (0, import_react.useState)(0);
|
|
1834
906
|
(0, import_react.useEffect)(() => {
|
|
907
|
+
const unsubscribe = subscribeBridge(() => refresh((value) => value + 1));
|
|
1835
908
|
const timer = setInterval(() => refresh((value) => value + 1), 5e3);
|
|
1836
|
-
return () =>
|
|
909
|
+
return () => {
|
|
910
|
+
unsubscribe();
|
|
911
|
+
clearInterval(timer);
|
|
912
|
+
};
|
|
1837
913
|
}, []);
|
|
1838
914
|
const stats = readTodayStats();
|
|
1839
915
|
const hasStats = stats.sent + stats.skipped + stats.recovered + stats.failed + stats.gaveUp + stats.looped > 0;
|
|
@@ -1940,8 +1016,8 @@ function LivePanels(props) {
|
|
|
1940
1016
|
}
|
|
1941
1017
|
function AutoContinueSettingsCard(props) {
|
|
1942
1018
|
const { t } = props;
|
|
1943
|
-
const
|
|
1944
|
-
const disabled = !
|
|
1019
|
+
const state2 = props.useAutoContinueSettingsCard((snapshot) => snapshot);
|
|
1020
|
+
const disabled = !state2.writable;
|
|
1945
1021
|
const shared = { t, disabled };
|
|
1946
1022
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1947
1023
|
SettingsCard,
|
|
@@ -1949,7 +1025,7 @@ function AutoContinueSettingsCard(props) {
|
|
|
1949
1025
|
t,
|
|
1950
1026
|
titleKey: "card.title",
|
|
1951
1027
|
descriptionKey: "card.description",
|
|
1952
|
-
state,
|
|
1028
|
+
state: state2,
|
|
1953
1029
|
onSave: props.save,
|
|
1954
1030
|
onDiscard: props.discard,
|
|
1955
1031
|
children: [
|
|
@@ -1960,7 +1036,7 @@ function AutoContinueSettingsCard(props) {
|
|
|
1960
1036
|
label: t("field.paused"),
|
|
1961
1037
|
hint: t("field.pausedHint"),
|
|
1962
1038
|
...shared,
|
|
1963
|
-
...
|
|
1039
|
+
...state2.paused,
|
|
1964
1040
|
onEdit: (text) => props.edit("paused", text),
|
|
1965
1041
|
onReset: () => props.resetField("paused")
|
|
1966
1042
|
}
|
|
@@ -1972,7 +1048,7 @@ function AutoContinueSettingsCard(props) {
|
|
|
1972
1048
|
label: t("field.continueText"),
|
|
1973
1049
|
hint: t("field.continueTextHint"),
|
|
1974
1050
|
...shared,
|
|
1975
|
-
...
|
|
1051
|
+
...state2.continueText,
|
|
1976
1052
|
onEdit: (text) => props.edit("continueText", text),
|
|
1977
1053
|
placeholder: DEFAULT_CONFIG.continueText,
|
|
1978
1054
|
onReset: () => props.resetField("continueText")
|
|
@@ -1985,7 +1061,7 @@ function AutoContinueSettingsCard(props) {
|
|
|
1985
1061
|
label: t("field.continueTextMaxTokens"),
|
|
1986
1062
|
hint: t("field.continueTextMaxTokensHint"),
|
|
1987
1063
|
...shared,
|
|
1988
|
-
...
|
|
1064
|
+
...state2.continueTextMaxTokens,
|
|
1989
1065
|
onEdit: (text) => props.edit("continueTextMaxTokens", text),
|
|
1990
1066
|
placeholder: DEFAULT_CONFIG.continueTextMaxTokens,
|
|
1991
1067
|
onReset: () => props.resetField("continueTextMaxTokens")
|
|
@@ -1998,7 +1074,7 @@ function AutoContinueSettingsCard(props) {
|
|
|
1998
1074
|
label: t("field.guardTools"),
|
|
1999
1075
|
hint: t("field.guardToolsHint"),
|
|
2000
1076
|
...shared,
|
|
2001
|
-
...
|
|
1077
|
+
...state2.guardTools,
|
|
2002
1078
|
onEdit: (text) => props.edit("guardTools", text),
|
|
2003
1079
|
onReset: () => props.resetField("guardTools")
|
|
2004
1080
|
}
|
|
@@ -2010,7 +1086,7 @@ function AutoContinueSettingsCard(props) {
|
|
|
2010
1086
|
label: t("field.guardPendingText"),
|
|
2011
1087
|
hint: t("field.guardPendingTextHint"),
|
|
2012
1088
|
...shared,
|
|
2013
|
-
...
|
|
1089
|
+
...state2.guardPendingText,
|
|
2014
1090
|
onEdit: (text) => props.edit("guardPendingText", text),
|
|
2015
1091
|
placeholder: DEFAULT_CONFIG.guardPendingText,
|
|
2016
1092
|
onReset: () => props.resetField("guardPendingText")
|
|
@@ -2023,7 +1099,7 @@ function AutoContinueSettingsCard(props) {
|
|
|
2023
1099
|
label: t("field.guardDoneText"),
|
|
2024
1100
|
hint: t("field.guardDoneTextHint"),
|
|
2025
1101
|
...shared,
|
|
2026
|
-
...
|
|
1102
|
+
...state2.guardDoneText,
|
|
2027
1103
|
onEdit: (text) => props.edit("guardDoneText", text),
|
|
2028
1104
|
placeholder: DEFAULT_CONFIG.guardDoneText,
|
|
2029
1105
|
onReset: () => props.resetField("guardDoneText")
|
|
@@ -2037,7 +1113,7 @@ function AutoContinueSettingsCard(props) {
|
|
|
2037
1113
|
hint: t("field.graceMsHint"),
|
|
2038
1114
|
numeric: true,
|
|
2039
1115
|
...shared,
|
|
2040
|
-
...
|
|
1116
|
+
...state2.graceMs,
|
|
2041
1117
|
onEdit: (text) => props.edit("graceMs", text),
|
|
2042
1118
|
onReset: () => props.resetField("graceMs")
|
|
2043
1119
|
}
|
|
@@ -2050,7 +1126,7 @@ function AutoContinueSettingsCard(props) {
|
|
|
2050
1126
|
hint: t("field.cooldownMsHint"),
|
|
2051
1127
|
numeric: true,
|
|
2052
1128
|
...shared,
|
|
2053
|
-
...
|
|
1129
|
+
...state2.cooldownMs,
|
|
2054
1130
|
onEdit: (text) => props.edit("cooldownMs", text),
|
|
2055
1131
|
onReset: () => props.resetField("cooldownMs")
|
|
2056
1132
|
}
|
|
@@ -2063,7 +1139,7 @@ function AutoContinueSettingsCard(props) {
|
|
|
2063
1139
|
hint: t("field.maxConsecutiveHint"),
|
|
2064
1140
|
numeric: true,
|
|
2065
1141
|
...shared,
|
|
2066
|
-
...
|
|
1142
|
+
...state2.maxConsecutive,
|
|
2067
1143
|
onEdit: (text) => props.edit("maxConsecutive", text),
|
|
2068
1144
|
onReset: () => props.resetField("maxConsecutive")
|
|
2069
1145
|
}
|
|
@@ -2075,7 +1151,7 @@ function AutoContinueSettingsCard(props) {
|
|
|
2075
1151
|
label: t("field.scanOnBoot"),
|
|
2076
1152
|
hint: t("field.scanOnBootHint"),
|
|
2077
1153
|
...shared,
|
|
2078
|
-
...
|
|
1154
|
+
...state2.scanOnBoot,
|
|
2079
1155
|
onEdit: (text) => props.edit("scanOnBoot", text),
|
|
2080
1156
|
onReset: () => props.resetField("scanOnBoot")
|
|
2081
1157
|
}
|
|
@@ -2088,7 +1164,7 @@ function AutoContinueSettingsCard(props) {
|
|
|
2088
1164
|
hint: t("field.scanLimitHint"),
|
|
2089
1165
|
numeric: true,
|
|
2090
1166
|
...shared,
|
|
2091
|
-
...
|
|
1167
|
+
...state2.scanLimit,
|
|
2092
1168
|
onEdit: (text) => props.edit("scanLimit", text),
|
|
2093
1169
|
onReset: () => props.resetField("scanLimit")
|
|
2094
1170
|
}
|
|
@@ -2101,7 +1177,7 @@ function AutoContinueSettingsCard(props) {
|
|
|
2101
1177
|
hint: t("field.freshMsHint"),
|
|
2102
1178
|
numeric: true,
|
|
2103
1179
|
...shared,
|
|
2104
|
-
...
|
|
1180
|
+
...state2.freshMs,
|
|
2105
1181
|
onEdit: (text) => props.edit("freshMs", text),
|
|
2106
1182
|
onReset: () => props.resetField("freshMs")
|
|
2107
1183
|
}
|
|
@@ -2114,7 +1190,7 @@ function AutoContinueSettingsCard(props) {
|
|
|
2114
1190
|
hint: t("field.reconnectScanDelayMsHint"),
|
|
2115
1191
|
numeric: true,
|
|
2116
1192
|
...shared,
|
|
2117
|
-
...
|
|
1193
|
+
...state2.reconnectScanDelayMs,
|
|
2118
1194
|
onEdit: (text) => props.edit("reconnectScanDelayMs", text),
|
|
2119
1195
|
onReset: () => props.resetField("reconnectScanDelayMs")
|
|
2120
1196
|
}
|
|
@@ -2127,7 +1203,7 @@ function AutoContinueSettingsCard(props) {
|
|
|
2127
1203
|
hint: t("field.reconnectBackoffMsHint"),
|
|
2128
1204
|
numeric: true,
|
|
2129
1205
|
...shared,
|
|
2130
|
-
...
|
|
1206
|
+
...state2.reconnectBackoffMs,
|
|
2131
1207
|
onEdit: (text) => props.edit("reconnectBackoffMs", text),
|
|
2132
1208
|
onReset: () => props.resetField("reconnectBackoffMs")
|
|
2133
1209
|
}
|
|
@@ -2139,7 +1215,7 @@ function AutoContinueSettingsCard(props) {
|
|
|
2139
1215
|
label: t("field.verbose"),
|
|
2140
1216
|
hint: t("field.verboseHint"),
|
|
2141
1217
|
...shared,
|
|
2142
|
-
...
|
|
1218
|
+
...state2.verbose,
|
|
2143
1219
|
onEdit: (text) => props.edit("verbose", text),
|
|
2144
1220
|
onReset: () => props.resetField("verbose")
|
|
2145
1221
|
}
|
|
@@ -2151,7 +1227,7 @@ function AutoContinueSettingsCard(props) {
|
|
|
2151
1227
|
label: t("field.classify"),
|
|
2152
1228
|
hint: t("field.classifyHint"),
|
|
2153
1229
|
...shared,
|
|
2154
|
-
...
|
|
1230
|
+
...state2.classify,
|
|
2155
1231
|
onEdit: (text) => props.edit("classify", text),
|
|
2156
1232
|
onReset: () => props.resetField("classify")
|
|
2157
1233
|
}
|
|
@@ -2164,7 +1240,7 @@ function AutoContinueSettingsCard(props) {
|
|
|
2164
1240
|
hint: t("field.backoffFactorHint"),
|
|
2165
1241
|
numeric: true,
|
|
2166
1242
|
...shared,
|
|
2167
|
-
...
|
|
1243
|
+
...state2.backoffFactor,
|
|
2168
1244
|
onEdit: (text) => props.edit("backoffFactor", text),
|
|
2169
1245
|
onReset: () => props.resetField("backoffFactor")
|
|
2170
1246
|
}
|
|
@@ -2177,7 +1253,7 @@ function AutoContinueSettingsCard(props) {
|
|
|
2177
1253
|
hint: t("field.backoffMaxMsHint"),
|
|
2178
1254
|
numeric: true,
|
|
2179
1255
|
...shared,
|
|
2180
|
-
...
|
|
1256
|
+
...state2.backoffMaxMs,
|
|
2181
1257
|
onEdit: (text) => props.edit("backoffMaxMs", text),
|
|
2182
1258
|
onReset: () => props.resetField("backoffMaxMs")
|
|
2183
1259
|
}
|
|
@@ -2189,7 +1265,7 @@ function AutoContinueSettingsCard(props) {
|
|
|
2189
1265
|
label: t("field.notify"),
|
|
2190
1266
|
hint: t("field.notifyHint"),
|
|
2191
1267
|
...shared,
|
|
2192
|
-
...
|
|
1268
|
+
...state2.notify,
|
|
2193
1269
|
onEdit: (text) => props.edit("notify", text),
|
|
2194
1270
|
onReset: () => props.resetField("notify")
|
|
2195
1271
|
}
|
|
@@ -2201,7 +1277,7 @@ function AutoContinueSettingsCard(props) {
|
|
|
2201
1277
|
label: t("field.loopGuard"),
|
|
2202
1278
|
hint: t("field.loopGuardHint"),
|
|
2203
1279
|
...shared,
|
|
2204
|
-
...
|
|
1280
|
+
...state2.loopGuard,
|
|
2205
1281
|
onEdit: (text) => props.edit("loopGuard", text),
|
|
2206
1282
|
onReset: () => props.resetField("loopGuard")
|
|
2207
1283
|
}
|
|
@@ -2214,7 +1290,7 @@ function AutoContinueSettingsCard(props) {
|
|
|
2214
1290
|
hint: t("field.loopShortCharsHint"),
|
|
2215
1291
|
numeric: true,
|
|
2216
1292
|
...shared,
|
|
2217
|
-
...
|
|
1293
|
+
...state2.loopShortChars,
|
|
2218
1294
|
onEdit: (text) => props.edit("loopShortChars", text),
|
|
2219
1295
|
onReset: () => props.resetField("loopShortChars")
|
|
2220
1296
|
}
|
|
@@ -2227,7 +1303,7 @@ function AutoContinueSettingsCard(props) {
|
|
|
2227
1303
|
hint: t("field.loopWindowMsHint"),
|
|
2228
1304
|
numeric: true,
|
|
2229
1305
|
...shared,
|
|
2230
|
-
...
|
|
1306
|
+
...state2.loopWindowMs,
|
|
2231
1307
|
onEdit: (text) => props.edit("loopWindowMs", text),
|
|
2232
1308
|
onReset: () => props.resetField("loopWindowMs")
|
|
2233
1309
|
}
|
|
@@ -2240,7 +1316,7 @@ function AutoContinueSettingsCard(props) {
|
|
|
2240
1316
|
hint: t("field.loopShortCountHint"),
|
|
2241
1317
|
numeric: true,
|
|
2242
1318
|
...shared,
|
|
2243
|
-
...
|
|
1319
|
+
...state2.loopShortCount,
|
|
2244
1320
|
onEdit: (text) => props.edit("loopShortCount", text),
|
|
2245
1321
|
onReset: () => props.resetField("loopShortCount")
|
|
2246
1322
|
}
|
|
@@ -2253,7 +1329,7 @@ function AutoContinueSettingsCard(props) {
|
|
|
2253
1329
|
hint: t("field.loopToolRepeatHint"),
|
|
2254
1330
|
numeric: true,
|
|
2255
1331
|
...shared,
|
|
2256
|
-
...
|
|
1332
|
+
...state2.loopToolRepeat,
|
|
2257
1333
|
onEdit: (text) => props.edit("loopToolRepeat", text),
|
|
2258
1334
|
onReset: () => props.resetField("loopToolRepeat")
|
|
2259
1335
|
}
|
|
@@ -2266,7 +1342,7 @@ function AutoContinueSettingsCard(props) {
|
|
|
2266
1342
|
hint: t("field.loopRepeatTextHint"),
|
|
2267
1343
|
numeric: true,
|
|
2268
1344
|
...shared,
|
|
2269
|
-
...
|
|
1345
|
+
...state2.loopRepeatText,
|
|
2270
1346
|
onEdit: (text) => props.edit("loopRepeatText", text),
|
|
2271
1347
|
onReset: () => props.resetField("loopRepeatText")
|
|
2272
1348
|
}
|
|
@@ -2278,7 +1354,7 @@ function AutoContinueSettingsCard(props) {
|
|
|
2278
1354
|
label: t("field.loopText"),
|
|
2279
1355
|
hint: t("field.loopTextHint"),
|
|
2280
1356
|
...shared,
|
|
2281
|
-
...
|
|
1357
|
+
...state2.loopText,
|
|
2282
1358
|
onEdit: (text) => props.edit("loopText", text),
|
|
2283
1359
|
placeholder: DEFAULT_CONFIG.loopText,
|
|
2284
1360
|
onReset: () => props.resetField("loopText")
|
|
@@ -2293,13 +1369,11 @@ function AutoContinueSettingsCard(props) {
|
|
|
2293
1369
|
// src/client/index.ts
|
|
2294
1370
|
var NS = "auto-continue";
|
|
2295
1371
|
var SETTINGS_NS = "auto-continue";
|
|
2296
|
-
var inject = ["slots", "locale", "
|
|
2297
|
-
var current = null;
|
|
1372
|
+
var inject = ["slots", "locale", "settingsScope"];
|
|
2298
1373
|
function apply(ctx) {
|
|
2299
1374
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "auto-continue: dictionaries");
|
|
1375
|
+
ctx.effect(() => startBridge(), "auto-continue: host bridge");
|
|
2300
1376
|
const scope = ctx.settingsScope.bind({ namespace: SETTINGS_NS });
|
|
2301
|
-
current?.dispose();
|
|
2302
|
-
current = new AutoContinueRunner(ctx.connection.api, () => resolveConfig(scope.getSnapshot().value));
|
|
2303
1377
|
const controller = new AutoContinueSettingsCardController(scope);
|
|
2304
1378
|
ctx.slots.inject(
|
|
2305
1379
|
"settings.plugin.item",
|