dsh-input-assistant 0.1.3 → 0.1.4
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/CHANGELOG.md +83 -0
- package/LICENSE +21 -21
- package/README.en.md +100 -0
- package/README.md +215 -184
- package/cordis.patch.yml +3 -3
- package/docs/CHANGELOG-0.1.2.md +102 -102
- package/docs/evaluation.md +111 -111
- package/docs/images/dark.png +0 -0
- package/docs/images/light.png +0 -0
- package/docs/images/optimize.png +0 -0
- package/docs/references.md +39 -39
- package/docs/smoke-comparison.md +37 -37
- package/docs/validation.md +180 -100
- package/lib/browser/components.d.ts +1 -1
- package/lib/browser/controller.d.ts +47 -4
- package/lib/browser/dom.d.ts +1 -1
- package/lib/browser/index.d.ts +1 -1
- package/lib/browser/public.d.ts +2 -2
- package/lib/browser/runtime.d.ts +4 -2
- package/lib/browser/settings.d.ts +1 -1
- package/lib/browser/styles.d.ts +1 -1
- package/lib/client.js +1164 -222
- package/lib/client.meta.json +40 -40
- package/lib/core/config.d.ts +1 -1
- package/lib/core/context.d.ts +1 -1
- package/lib/core/generation.d.ts +2 -2
- package/lib/core/index.d.ts +9 -8
- package/lib/core/models.d.ts +15 -0
- package/lib/core/protection.d.ts +3 -4
- package/lib/core/scheduler.d.ts +1 -1
- package/lib/core/snapshot.d.ts +1 -1
- package/lib/core/types.d.ts +11 -0
- package/lib/core.js +280 -53
- package/lib/core.js.map +2 -2
- package/lib/host/rpc.d.ts +2 -0
- package/lib/host/runtime.d.ts +5 -3
- package/lib/host/storage.d.ts +11 -3
- package/lib/host/transcript.d.ts +1 -1
- package/lib/index.d.ts +5 -3
- package/lib/index.js +451 -156
- package/lib/index.js.map +4 -4
- package/package.json +10 -3
package/lib/core.js
CHANGED
|
@@ -5,6 +5,7 @@ var DEFAULT_CONFIG = Object.freeze({
|
|
|
5
5
|
optimizationStrength: "standard",
|
|
6
6
|
nextEnabled: true,
|
|
7
7
|
completionEnabled: true,
|
|
8
|
+
predictionDetail: "standard",
|
|
8
9
|
displayMode: "ghost",
|
|
9
10
|
debounceMs: 600,
|
|
10
11
|
minIntervalMs: 2e3,
|
|
@@ -33,8 +34,8 @@ var numeric = {
|
|
|
33
34
|
minIntervalMs: [500, 3e4],
|
|
34
35
|
maxHistoryChars: [1e3, 64e3],
|
|
35
36
|
maxDraftChars: [100, 32e3],
|
|
36
|
-
maxNextChars: [20,
|
|
37
|
-
maxCompletionChars: [10,
|
|
37
|
+
maxNextChars: [20, 1e3],
|
|
38
|
+
maxCompletionChars: [10, 500],
|
|
38
39
|
optimizeTimeoutMs: [1e3, 12e4],
|
|
39
40
|
suggestionTimeoutMs: [1e3, 6e4]
|
|
40
41
|
};
|
|
@@ -46,6 +47,7 @@ function validate(config) {
|
|
|
46
47
|
}
|
|
47
48
|
if (!["ghost", "panel"].includes(config.displayMode)) throw new AssistantError("CONFIG", "\u663E\u793A\u65B9\u5F0F\u5FC5\u987B\u662F ghost \u6216 panel");
|
|
48
49
|
if (!["light", "standard", "deep"].includes(config.optimizationStrength)) throw new AssistantError("CONFIG", "\u4F18\u5316\u5F3A\u5EA6\u5FC5\u987B\u662F light\u3001standard \u6216 deep");
|
|
50
|
+
if (!["concise", "standard", "detailed"].includes(config.predictionDetail)) throw new AssistantError("CONFIG", "\u9884\u6D4B\u8BE6\u7565\u5EA6\u5FC5\u987B\u662F concise\u3001standard \u6216 detailed");
|
|
49
51
|
for (const [key, [min, max]] of Object.entries(numeric)) {
|
|
50
52
|
const value = config[key];
|
|
51
53
|
if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) {
|
|
@@ -67,6 +69,7 @@ function resolveStoredConfig(input) {
|
|
|
67
69
|
const record = input && typeof input === "object" && !Array.isArray(input) ? input : {};
|
|
68
70
|
const config = { ...DEFAULT_CONFIG };
|
|
69
71
|
for (const key of Object.keys(DEFAULT_CONFIG)) {
|
|
72
|
+
if (key === "provider" || key === "model") continue;
|
|
70
73
|
if (!(key in record)) continue;
|
|
71
74
|
const value = record[key];
|
|
72
75
|
const candidate = { ...DEFAULT_CONFIG, [key]: value };
|
|
@@ -77,9 +80,11 @@ function resolveStoredConfig(input) {
|
|
|
77
80
|
}
|
|
78
81
|
config[key] = typeof value === "string" ? value.trim() : value;
|
|
79
82
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
config.
|
|
83
|
+
try {
|
|
84
|
+
const route = validate({ ...DEFAULT_CONFIG, provider: record.provider ?? "", model: record.model ?? "" });
|
|
85
|
+
config.provider = route.provider;
|
|
86
|
+
config.model = route.model;
|
|
87
|
+
} catch {
|
|
83
88
|
}
|
|
84
89
|
return config;
|
|
85
90
|
}
|
|
@@ -117,7 +122,7 @@ function buildContext(history, budget = 16e3) {
|
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
// src/core/protection.ts
|
|
120
|
-
var FILE_EXTENSIONS = [
|
|
125
|
+
var FILE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
121
126
|
"json",
|
|
122
127
|
"jsonc",
|
|
123
128
|
"json5",
|
|
@@ -207,8 +212,8 @@ var FILE_EXTENSIONS = [
|
|
|
207
212
|
"stylelintrc",
|
|
208
213
|
"babelrc",
|
|
209
214
|
"browserslistrc"
|
|
210
|
-
]
|
|
211
|
-
var NAMED_FILES = [
|
|
215
|
+
]);
|
|
216
|
+
var NAMED_FILES = /* @__PURE__ */ new Set([
|
|
212
217
|
"LICENSE",
|
|
213
218
|
"LICENCE",
|
|
214
219
|
"README",
|
|
@@ -225,28 +230,193 @@ var NAMED_FILES = [
|
|
|
225
230
|
"Vagrantfile",
|
|
226
231
|
"Jenkinsfile",
|
|
227
232
|
"Brewfile"
|
|
228
|
-
]
|
|
229
|
-
var
|
|
230
|
-
|
|
233
|
+
]);
|
|
234
|
+
var DOTFILES = [
|
|
235
|
+
".env",
|
|
236
|
+
".npmrc",
|
|
237
|
+
".nvmrc",
|
|
238
|
+
".editorconfig",
|
|
239
|
+
".gitignore",
|
|
240
|
+
".gitattributes",
|
|
241
|
+
".gitmodules",
|
|
242
|
+
".prettierrc",
|
|
243
|
+
".eslintrc",
|
|
244
|
+
".stylelintrc",
|
|
245
|
+
".babelrc",
|
|
246
|
+
".browserslistrc"
|
|
247
|
+
];
|
|
248
|
+
var candidatePattern = /[\p{L}\p{N}\p{M}_.@+~\\/-]+/uy;
|
|
249
|
+
var cjk = "[\\p{Script=Han}\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Hangul}]";
|
|
250
|
+
var cjkCharacter = new RegExp(cjk, "u");
|
|
251
|
+
var candidateEnding = new RegExp(`(?:\\.[A-Za-z][A-Za-z0-9]{0,11}|${[...NAMED_FILES].join("|")})(?=${cjk}|$)`, "gu");
|
|
252
|
+
function knownFile(value) {
|
|
253
|
+
const dot = value.lastIndexOf(".");
|
|
254
|
+
return NAMED_FILES.has(value) || DOTFILES.some((name) => value === name || value.startsWith(name + ".")) || dot > 0 && FILE_EXTENSIONS.has(value.slice(dot + 1).toLowerCase());
|
|
255
|
+
}
|
|
256
|
+
function pathOrFile(value) {
|
|
257
|
+
const slash = Math.max(value.lastIndexOf("/"), value.lastIndexOf("\\"));
|
|
258
|
+
if (slash < 0) return knownFile(value);
|
|
259
|
+
if (/^(?:\.{1,2}[\\/]|[\\/])/.test(value)) return value.length > slash + 1 || slash > 0;
|
|
260
|
+
const tail = value.slice(slash + 1), dot = tail.lastIndexOf(".");
|
|
261
|
+
if (dot >= 0 && /^[A-Za-z][A-Za-z0-9]{0,11}$/.test(tail.slice(dot + 1))) return true;
|
|
262
|
+
let separators = 0;
|
|
263
|
+
for (const char of value) if (char === "/" || char === "\\") separators++;
|
|
264
|
+
return separators >= 2;
|
|
265
|
+
}
|
|
266
|
+
function literalStart(value) {
|
|
267
|
+
const slash = value.search(/[\\/]/);
|
|
268
|
+
const first = slash < 0 ? value : value.slice(0, slash);
|
|
269
|
+
if (slash >= 0) {
|
|
270
|
+
const relative = first.match(/(?:\.{1,2}|~)$/);
|
|
271
|
+
if (relative) return relative.index;
|
|
272
|
+
}
|
|
273
|
+
const dot = first.indexOf(".");
|
|
274
|
+
const stem = dot < 0 ? first : first.slice(0, dot);
|
|
275
|
+
let start = 0, previous = "", offset = 0;
|
|
276
|
+
for (const char of stem) {
|
|
277
|
+
if (cjkCharacter.test(previous) && /[A-Za-z0-9_@+~-]/.test(char)) start = offset;
|
|
278
|
+
previous = char;
|
|
279
|
+
offset += char.length;
|
|
280
|
+
}
|
|
281
|
+
if (!start && dot > 0 && DOTFILES.some((name) => first.slice(dot) === name || first.slice(dot).startsWith(name + ".")) && Array.from(stem).every((char) => cjkCharacter.test(char))) return dot;
|
|
282
|
+
return start;
|
|
283
|
+
}
|
|
284
|
+
function candidateRanges(value, base) {
|
|
285
|
+
const ranges = [];
|
|
286
|
+
let cursor = 0;
|
|
287
|
+
candidateEnding.lastIndex = 0;
|
|
288
|
+
for (const ending of value.matchAll(candidateEnding)) {
|
|
289
|
+
const end = ending.index + ending[0].length;
|
|
290
|
+
let next = end;
|
|
291
|
+
while (next < value.length) {
|
|
292
|
+
const char = String.fromCodePoint(value.codePointAt(next));
|
|
293
|
+
if (!cjkCharacter.test(char)) break;
|
|
294
|
+
next += char.length;
|
|
295
|
+
}
|
|
296
|
+
if (next > end && /[\\/]/.test(value[next] ?? "")) continue;
|
|
297
|
+
const portion = value.slice(cursor, end);
|
|
298
|
+
const start = cursor + literalStart(portion);
|
|
299
|
+
if (pathOrFile(value.slice(start, end))) ranges.push({ start: base + start, end: base + end });
|
|
300
|
+
cursor = end;
|
|
301
|
+
}
|
|
302
|
+
if (cursor < value.length && /[.\\/]/.test(value.slice(cursor))) {
|
|
303
|
+
const portion = value.slice(cursor);
|
|
304
|
+
const start = cursor + literalStart(portion);
|
|
305
|
+
let end = value.length;
|
|
306
|
+
const tailStart = Math.max(start, Math.max(value.lastIndexOf("/"), value.lastIndexOf("\\")) + 1);
|
|
307
|
+
const tail = value.slice(tailStart), boundary = tail.search(cjkCharacter);
|
|
308
|
+
if (tailStart >= start && boundary > 0 && /^[A-Za-z0-9_@+~.-]/.test(tail)) end = tailStart + boundary;
|
|
309
|
+
if (pathOrFile(value.slice(start, end))) ranges.push({ start: base + start, end: base + end });
|
|
310
|
+
}
|
|
311
|
+
return ranges;
|
|
312
|
+
}
|
|
313
|
+
function urlEnd(text, start) {
|
|
314
|
+
let end = start, parentheses = 0, brackets = 0;
|
|
315
|
+
while (end < text.length) {
|
|
316
|
+
const char = text[end];
|
|
317
|
+
if (/[\s<>"'`,。;;)]/.test(char)) break;
|
|
318
|
+
if (char === "(") parentheses++;
|
|
319
|
+
else if (char === ")") {
|
|
320
|
+
if (!parentheses) break;
|
|
321
|
+
parentheses--;
|
|
322
|
+
} else if (char === "[") brackets++;
|
|
323
|
+
else if (char === "]") {
|
|
324
|
+
if (!brackets) break;
|
|
325
|
+
brackets--;
|
|
326
|
+
}
|
|
327
|
+
end++;
|
|
328
|
+
}
|
|
329
|
+
while (end > start && /[.,!]/.test(text[end - 1])) end--;
|
|
330
|
+
return end;
|
|
331
|
+
}
|
|
332
|
+
function windowsPathEnd(text, start) {
|
|
333
|
+
const quote = text[start - 1];
|
|
334
|
+
if (quote === '"' || quote === "'") {
|
|
335
|
+
const closing = text.indexOf(quote, start);
|
|
336
|
+
if (closing >= 0 && !/[\r\n]/.test(text.slice(start, closing))) return closing;
|
|
337
|
+
}
|
|
338
|
+
let end = start + 3;
|
|
339
|
+
while (end < text.length && !/[\r\n<>"'`,。;;))\]!?!?::]/.test(text[end])) end++;
|
|
340
|
+
while (end > start && /[ \t]/.test(text[end - 1])) end--;
|
|
341
|
+
const value = text.slice(start, end);
|
|
342
|
+
const tailStart = Math.max(value.lastIndexOf("/"), value.lastIndexOf("\\")) + 1;
|
|
343
|
+
const tail = value.slice(tailStart);
|
|
344
|
+
const dotfile = DOTFILES.some((name) => tail.startsWith(name) && /[. \t]/.test(tail[name.length] ?? ""));
|
|
345
|
+
let dot = -1;
|
|
346
|
+
for (let i = 0; i < tail.length; i++) {
|
|
347
|
+
if (tail[i] === ".") dot = i;
|
|
348
|
+
if (/[ \t]/.test(tail[i]) && (dotfile || dot >= 0 && i - dot <= 13 && FILE_EXTENSIONS.has(tail.slice(dot + 1, i).toLowerCase()) || i <= 20 && NAMED_FILES.has(tail.slice(0, i)))) return start + tailStart + i;
|
|
349
|
+
}
|
|
350
|
+
return end;
|
|
351
|
+
}
|
|
352
|
+
function automaticRanges(text) {
|
|
231
353
|
const ranges = [];
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
354
|
+
let cursor = 0;
|
|
355
|
+
while (cursor < text.length) {
|
|
356
|
+
const start = cursor;
|
|
357
|
+
const fence = text.startsWith("```", cursor) ? "```" : text.startsWith("~~~", cursor) ? "~~~" : null;
|
|
358
|
+
if (fence) {
|
|
359
|
+
const closing = text.indexOf(fence, cursor + 3);
|
|
360
|
+
cursor = closing < 0 ? text.length : closing + 3;
|
|
361
|
+
ranges.push({ start, end: cursor });
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (text[cursor] === "`") {
|
|
365
|
+
let end = cursor + 1;
|
|
366
|
+
while (end < text.length && text[end] !== "`" && text[end] !== "\n") end++;
|
|
367
|
+
if (text[end] === "`" && end > cursor + 1) {
|
|
368
|
+
cursor = end + 1;
|
|
369
|
+
ranges.push({ start, end: cursor });
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (text[cursor] === '"' || text[cursor] === "'") {
|
|
374
|
+
const closing = text.indexOf(text[cursor], cursor + 1);
|
|
375
|
+
if (closing >= 0) {
|
|
376
|
+
const literal = text.slice(cursor + 1, closing);
|
|
377
|
+
if (!/[\r\n]/.test(literal) && (pathOrFile(literal) || /^[A-Za-z]:[\\/]/.test(literal))) {
|
|
378
|
+
ranges.push({ start: cursor + 1, end: closing });
|
|
379
|
+
cursor = closing + 1;
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const scheme = text.slice(cursor, cursor + 8).toLowerCase();
|
|
385
|
+
if (scheme.startsWith("https://") || scheme.startsWith("http://")) {
|
|
386
|
+
cursor = urlEnd(text, cursor);
|
|
387
|
+
ranges.push({ start, end: cursor });
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (/[A-Za-z]/.test(text[cursor]) && text[cursor + 1] === ":" && /[\\/]/.test(text[cursor + 2] ?? "")) {
|
|
391
|
+
cursor = windowsPathEnd(text, cursor);
|
|
392
|
+
ranges.push({ start, end: cursor });
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
candidatePattern.lastIndex = cursor;
|
|
396
|
+
const match = candidatePattern.exec(text);
|
|
397
|
+
if (match) {
|
|
398
|
+
cursor = candidatePattern.lastIndex;
|
|
399
|
+
const fenceOffset = match[0].indexOf("~~~");
|
|
400
|
+
if (fenceOffset >= 0) cursor = start + fenceOffset;
|
|
401
|
+
else if (text[cursor] === ":") {
|
|
402
|
+
if (text.startsWith("//", cursor + 1)) {
|
|
403
|
+
const ending = text.slice(Math.max(start, cursor - 5), cursor).toLowerCase();
|
|
404
|
+
if (ending.endsWith("https")) cursor -= 5;
|
|
405
|
+
else if (ending.endsWith("http")) cursor -= 4;
|
|
406
|
+
else if (/[A-Za-z]/.test(text[cursor - 1] ?? "")) cursor--;
|
|
407
|
+
} else if (/[\\/]/.test(text[cursor + 1] ?? "") && /[A-Za-z]/.test(text[cursor - 1] ?? "")) cursor--;
|
|
408
|
+
}
|
|
409
|
+
if (cursor === start) continue;
|
|
410
|
+
let end = cursor;
|
|
411
|
+
if (!/[\\/]\.{1,2}$/.test(text.slice(start, cursor))) while (end > start && text[end - 1] === ".") end--;
|
|
412
|
+
const value = text.slice(start, end);
|
|
413
|
+
if (value) ranges.push(...candidateRanges(value, start));
|
|
414
|
+
} else cursor++;
|
|
415
|
+
}
|
|
416
|
+
return ranges;
|
|
417
|
+
}
|
|
418
|
+
function protectText(text, locked = []) {
|
|
419
|
+
const ranges = automaticRanges(text);
|
|
250
420
|
for (const literal of locked) {
|
|
251
421
|
if (typeof literal !== "string" || !literal.length || !text.includes(literal)) throw new AssistantError("LOCK_SCOPE", "\u9501\u5B9A\u7247\u6BB5\u5DF2\u53D8\u5316\u6216\u4E0D\u5728\u672C\u6B21\u4F18\u5316\u8303\u56F4\u5185");
|
|
252
422
|
let start = text.indexOf(literal);
|
|
@@ -262,8 +432,14 @@ function protectText(text, locked = []) {
|
|
|
262
432
|
if (prev && r.start < prev.end) prev.end = Math.max(prev.end, r.end);
|
|
263
433
|
else merged.push({ ...r });
|
|
264
434
|
}
|
|
265
|
-
let
|
|
266
|
-
while (
|
|
435
|
+
let longestPrefix = -1, occurrence = text.indexOf("DSH_KEEP_");
|
|
436
|
+
while (occurrence >= 0) {
|
|
437
|
+
let start = occurrence;
|
|
438
|
+
while (start > 0 && text[start - 1] === "_") start--;
|
|
439
|
+
longestPrefix = Math.max(longestPrefix, occurrence - start);
|
|
440
|
+
occurrence = text.indexOf("DSH_KEEP_", occurrence + 9);
|
|
441
|
+
}
|
|
442
|
+
const prefix = "_".repeat(longestPrefix + 1) + "DSH_KEEP_";
|
|
267
443
|
const fragments = [];
|
|
268
444
|
let cursor = 0, masked = "";
|
|
269
445
|
for (const r of merged) {
|
|
@@ -276,19 +452,20 @@ function protectText(text, locked = []) {
|
|
|
276
452
|
return { original: text, masked: masked + text.slice(cursor), fragments };
|
|
277
453
|
}
|
|
278
454
|
function restoreProtected(output, protectedText) {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
455
|
+
const fragments = new Map(protectedText.fragments.map((fragment) => [fragment.token, fragment.original]));
|
|
456
|
+
const existing = new Set(protectedText.original.match(/⟪_*DSH_KEEP_\d+⟫/gu) ?? []);
|
|
457
|
+
const seen = /* @__PURE__ */ new Set();
|
|
458
|
+
const restored = output.replace(/⟪_*DSH_KEEP_\d+⟫/gu, (token) => {
|
|
459
|
+
const original = fragments.get(token);
|
|
460
|
+
if (original === void 0) {
|
|
461
|
+
if (!existing.has(token)) throw new AssistantError("PROTECTION", "\u751F\u6210\u7ED3\u679C\u5305\u542B\u65E0\u6548\u7684\u4FDD\u62A4\u6807\u8BB0\uFF0C\u5DF2\u4FDD\u7559\u539F\u7A3F");
|
|
462
|
+
return token;
|
|
463
|
+
}
|
|
464
|
+
if (seen.has(token)) throw new AssistantError("PROTECTION", "\u751F\u6210\u7ED3\u679C\u672A\u5B8C\u6574\u4FDD\u7559\u9501\u5B9A\u5185\u5BB9\uFF0C\u5DF2\u4FDD\u7559\u539F\u7A3F");
|
|
465
|
+
seen.add(token);
|
|
466
|
+
return original;
|
|
467
|
+
});
|
|
468
|
+
if (seen.size !== fragments.size) throw new AssistantError("PROTECTION", "\u751F\u6210\u7ED3\u679C\u672A\u5B8C\u6574\u4FDD\u7559\u9501\u5B9A\u5185\u5BB9\uFF0C\u5DF2\u4FDD\u7559\u539F\u7A3F");
|
|
292
469
|
return restored;
|
|
293
470
|
}
|
|
294
471
|
|
|
@@ -309,6 +486,13 @@ function prepareGeneration(request, history, config) {
|
|
|
309
486
|
if (charCount(target) > config.maxDraftChars) throw new AssistantError("TOO_LONG", `\u672C\u6B21\u6587\u672C\u8D85\u8FC7 ${config.maxDraftChars} \u5B57\u7B26\uFF0C\u8BF7\u9009\u4E2D\u8F83\u77ED\u7247\u6BB5\u4F18\u5316`);
|
|
310
487
|
if (request.operation === "optimize" && !target.trim()) throw new AssistantError("EMPTY", "\u8BF7\u5148\u8F93\u5165\u6216\u9009\u4E2D\u9700\u8981\u4F18\u5316\u7684\u6587\u5B57");
|
|
311
488
|
if (request.operation === "complete" && !request.draft.trim()) throw new AssistantError("EMPTY", "\u6CA1\u6709\u53EF\u7EED\u5199\u7684\u8349\u7A3F");
|
|
489
|
+
const detail = config.predictionDetail;
|
|
490
|
+
const suggestionCap = request.operation === "next" ? config.maxNextChars : config.maxCompletionChars;
|
|
491
|
+
const detailGuidance = {
|
|
492
|
+
concise: "Detail level: CONCISE. Prefer one brief sentence or phrase. Do not add scope merely to fill the character budget.",
|
|
493
|
+
standard: "Detail level: STANDARD. Use a natural, complete expression with only the detail needed for the current intent.",
|
|
494
|
+
detailed: "Detail level: DETAILED. Explain only already-supported intent more fully; short paragraphs or a structured list are allowed when useful. There is no minimum length. Never add tasks, constraints, permissions, deliverables, or commitments merely to make the message longer."
|
|
495
|
+
}[detail];
|
|
312
496
|
let system = common, protection = null;
|
|
313
497
|
if (request.operation === "optimize") {
|
|
314
498
|
protection = protectText(target, request.lockedFragments);
|
|
@@ -333,18 +517,22 @@ function prepareGeneration(request, history, config) {
|
|
|
333
517
|
"Consider answering the latest question, a natural follow-up question, correcting a misunderstanding, or proceeding with an explicitly requested unfinished step.",
|
|
334
518
|
"A question or correction is allowed when supported. Do not automatically propose commits, pushing, testing, or new features just because code was produced.",
|
|
335
519
|
`If no single continuation is well-supported, or the task is naturally finished, output ${NO_SUGGESTION}. Do not guess an unresolved choice.`,
|
|
336
|
-
`Return one
|
|
520
|
+
`Return one message of at most ${suggestionCap} Unicode characters. This is a hard maximum, not a target or minimum.`,
|
|
521
|
+
detailGuidance,
|
|
522
|
+
detail === "detailed" ? "Internal line breaks are allowed for supported structure; do not add blank padding lines." : "Return a single line with no line breaks.",
|
|
337
523
|
"Examples: user requested hit rate AND invalidation, only hit rate was explained -> \u518D\u89E3\u91CA\u4E00\u4E0B\u7F13\u5B58\u5931\u6548\u3002",
|
|
338
524
|
"User said explanation only, assistant proposes file edits -> \u5148\u4E0D\u8981\u6539\u6587\u4EF6\uFF0C\u8BF7\u53EA\u89E3\u91CA\u539F\u56E0\u3002",
|
|
339
525
|
`Assistant asks Redis or local cache, no preference in history -> ${NO_SUGGESTION}.`
|
|
340
526
|
].join("\n");
|
|
341
527
|
} else {
|
|
342
528
|
system += "\n" + [
|
|
343
|
-
"Complete the user's current draft by returning ONLY a
|
|
529
|
+
"Complete the user's current draft by returning ONLY a suffix to append exactly at its end.",
|
|
344
530
|
"The draft is the strongest constraint. Do not repeat its prefix, rewrite existing text, introduce a new topic, or answer the question.",
|
|
345
531
|
"Preserve necessary leading whitespace in the suffix. A complete message does not need an arbitrary extra requirement.",
|
|
346
532
|
`If the intended continuation is uncertain or the draft is already complete, output ${NO_SUGGESTION}.`,
|
|
347
|
-
`Return at most ${
|
|
533
|
+
`Return at most ${suggestionCap} Unicode characters, with no explanation. This is a hard maximum, not a target or minimum.`,
|
|
534
|
+
detailGuidance,
|
|
535
|
+
detail === "detailed" ? "Line breaks are allowed only when they naturally continue the existing draft structure. Preserve a necessary leading line break, but do not start an unrelated list or task." : "Return a single line with no line breaks.",
|
|
348
536
|
"Examples: draft \u8BF7\u89E3\u91CA\u8FD9\u6BB5\u4EE3\u7801\u7684\u65F6\u95F4 -> \u590D\u6742\u5EA6\u3002",
|
|
349
537
|
`Draft Please explain the cause without editing files. -> ${NO_SUGGESTION}.`
|
|
350
538
|
].join("\n");
|
|
@@ -363,11 +551,46 @@ function prepareGeneration(request, history, config) {
|
|
|
363
551
|
config,
|
|
364
552
|
protection,
|
|
365
553
|
timeoutMs: request.operation === "optimize" ? config.optimizeTimeoutMs : config.suggestionTimeoutMs,
|
|
366
|
-
|
|
554
|
+
// A Unicode character may require several tokens, especially for CJK and
|
|
555
|
+
// emoji. Keep the transport budget large enough for the explicit text cap.
|
|
556
|
+
maxOutputTokens: request.operation === "optimize" ? 8192 : Math.max(request.operation === "next" ? 128 : 96, suggestionCap * 4 + 32)
|
|
367
557
|
};
|
|
368
558
|
}
|
|
559
|
+
function hasUnpairedSurrogate(text) {
|
|
560
|
+
for (let i = 0; i < text.length; i++) {
|
|
561
|
+
const code = text.charCodeAt(i);
|
|
562
|
+
if (code >= 55296 && code <= 56319) {
|
|
563
|
+
const next = text.charCodeAt(++i);
|
|
564
|
+
if (!(next >= 56320 && next <= 57343)) return true;
|
|
565
|
+
} else if (code >= 56320 && code <= 57343) return true;
|
|
566
|
+
}
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
function cleanOptimizationOutput(raw, prepared) {
|
|
570
|
+
let cleaned = raw.trim();
|
|
571
|
+
const original = prepared.protection.original.trim();
|
|
572
|
+
const masked = prepared.protection.masked.trim();
|
|
573
|
+
if (cleaned === masked) return cleaned;
|
|
574
|
+
const fence = cleaned.match(/^```(?:markdown|prompt|text)?\r?\n([\s\S]*?)\r?\n```$/i);
|
|
575
|
+
if (fence) cleaned = fence[1].trim();
|
|
576
|
+
const preamble = /^(?:(?:好的|好的,|已为您|根据您的要求|针对您的需求|我为您)?(?:优化|改写|整理|修改|调整)后的?(?:提示词|prompt|内容|表达|文本)?(?:如下|为)?[::\s\n]+|Here(?:'s| is) (?:the |an )?(?:optimized|improved|rewritten) (?:prompt|version)[::\s\n]+)/i;
|
|
577
|
+
const prefix = cleaned.match(preamble)?.[0];
|
|
578
|
+
if (prefix && !original.toLowerCase().startsWith(prefix.trim().toLowerCase())) cleaned = cleaned.slice(prefix.length).trim();
|
|
579
|
+
for (const [start, end] of [['"', '"'], ["\u201C", "\u201D"], ["`", "`"]]) {
|
|
580
|
+
if (!cleaned.startsWith(start) || !cleaned.endsWith(end) || cleaned.length < 2) continue;
|
|
581
|
+
const inner = cleaned.slice(start.length, -end.length).trim();
|
|
582
|
+
const originalWrapped = masked.startsWith(start) && masked.endsWith(end);
|
|
583
|
+
if (!originalWrapped || inner.startsWith(start) && inner.endsWith(end)) cleaned = inner;
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
return cleaned;
|
|
587
|
+
}
|
|
369
588
|
function finishGeneration(raw, prepared) {
|
|
370
589
|
if (typeof raw !== "string") throw new AssistantError("OUTPUT", "\u6A21\u578B\u672A\u8FD4\u56DE\u6709\u6548\u6587\u5B57");
|
|
590
|
+
if (hasUnpairedSurrogate(raw)) {
|
|
591
|
+
if (prepared.operation === "optimize") throw new AssistantError("OUTPUT", "\u6A21\u578B\u8FD4\u56DE\u4E86\u4E0D\u5B8C\u6574\u5B57\u7B26\uFF0C\u5DF2\u4FDD\u7559\u539F\u7A3F");
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
371
594
|
const trimmed = raw.trim();
|
|
372
595
|
if (!trimmed || trimmed === NO_SUGGESTION) {
|
|
373
596
|
if (prepared.operation === "optimize") throw new AssistantError("EMPTY_OUTPUT", "\u6A21\u578B\u672A\u8FD4\u56DE\u4F18\u5316\u6587\u5B57\uFF0C\u5DF2\u4FDD\u7559\u539F\u7A3F");
|
|
@@ -375,16 +598,20 @@ function finishGeneration(raw, prepared) {
|
|
|
375
598
|
}
|
|
376
599
|
if (prepared.operation === "optimize") {
|
|
377
600
|
if (raw.includes(NO_SUGGESTION)) throw new AssistantError("OUTPUT", "\u6A21\u578B\u8F93\u51FA\u4E0D\u5B8C\u6574\uFF0C\u5DF2\u4FDD\u7559\u539F\u7A3F");
|
|
378
|
-
const
|
|
601
|
+
const cleaned = cleanOptimizationOutput(trimmed, prepared);
|
|
602
|
+
if (!cleaned.trim()) throw new AssistantError("EMPTY_OUTPUT", "\u6A21\u578B\u672A\u8FD4\u56DE\u4F18\u5316\u6587\u5B57\uFF0C\u5DF2\u4FDD\u7559\u539F\u7A3F");
|
|
603
|
+
const output = restoreProtected(cleaned, prepared.protection);
|
|
604
|
+
if (hasUnpairedSurrogate(output)) throw new AssistantError("OUTPUT", "\u4F18\u5316\u7ED3\u679C\u5305\u542B\u4E0D\u5B8C\u6574\u5B57\u7B26\uFF0C\u5DF2\u4FDD\u7559\u539F\u7A3F");
|
|
379
605
|
if (charCount(output) > prepared.config.maxDraftChars * 2) throw new AssistantError("OUTPUT", "\u4F18\u5316\u7ED3\u679C\u8FC7\u957F\uFF0C\u5DF2\u4FDD\u7559\u539F\u7A3F");
|
|
380
606
|
return output;
|
|
381
607
|
}
|
|
382
608
|
const cap = prepared.operation === "next" ? prepared.config.maxNextChars : prepared.config.maxCompletionChars;
|
|
383
|
-
|
|
609
|
+
const allowNewlines = prepared.config.predictionDetail === "detailed";
|
|
610
|
+
if (charCount(raw) > cap || !allowNewlines && /[\r\n]/.test(raw) || /^(?:```|\{\s*"|<think>|(?:建议|预测|续写|Suggestion|Prediction)\s*[::])/i.test(trimmed) || trimmed.includes(NO_SUGGESTION)) return null;
|
|
384
611
|
if (prepared.operation === "next") return trimmed;
|
|
385
|
-
|
|
386
|
-
if (
|
|
387
|
-
return raw.replace(
|
|
612
|
+
const draft = prepared.request.draft.trim();
|
|
613
|
+
if (charCount(draft) >= 4 && trimmed.startsWith(draft) || prepared.request.draft.endsWith(raw)) return null;
|
|
614
|
+
return raw.replace(/\s+$/u, "");
|
|
388
615
|
}
|
|
389
616
|
|
|
390
617
|
// src/core/scheduler.ts
|