@martian-engineering/lossless-claw 0.4.0 → 0.5.1
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/package.json +2 -1
- package/src/assembler.ts +37 -3
- package/src/compaction.ts +83 -10
- package/src/db/connection.ts +2 -0
- package/src/db/migration.ts +84 -0
- package/src/engine.ts +657 -146
- package/src/large-files.ts +19 -0
- package/src/plugin/index.ts +188 -28
- package/src/store/conversation-store.ts +76 -10
- package/src/store/full-text-fallback.ts +9 -0
- package/src/store/index.ts +2 -0
- package/src/store/summary-store.ts +130 -10
- package/src/summarize.ts +209 -13
- package/src/types.ts +9 -0
package/src/large-files.ts
CHANGED
|
@@ -512,6 +512,25 @@ export function formatFileReference(input: {
|
|
|
512
512
|
].join("\n");
|
|
513
513
|
}
|
|
514
514
|
|
|
515
|
+
export function formatToolOutputReference(input: {
|
|
516
|
+
fileId: string;
|
|
517
|
+
toolName?: string;
|
|
518
|
+
byteSize: number;
|
|
519
|
+
summary: string;
|
|
520
|
+
}): string {
|
|
521
|
+
const toolName = input.toolName?.trim() || "unknown";
|
|
522
|
+
const byteSize = Math.max(0, input.byteSize);
|
|
523
|
+
|
|
524
|
+
return [
|
|
525
|
+
`[LCM Tool Output: ${input.fileId} | tool=${toolName} | ${byteSize.toLocaleString("en-US")} bytes]`,
|
|
526
|
+
"",
|
|
527
|
+
"Exploration Summary:",
|
|
528
|
+
input.summary.trim() || "(no summary available)",
|
|
529
|
+
"",
|
|
530
|
+
"Use lcm_describe with the file id to inspect the full output.",
|
|
531
|
+
].join("\n");
|
|
532
|
+
}
|
|
533
|
+
|
|
515
534
|
export async function generateExplorationSummary(input: ExplorationSummaryInput): Promise<string> {
|
|
516
535
|
const extension = extensionFromNameOrMime(input.fileName, input.mimeType);
|
|
517
536
|
|
package/src/plugin/index.ts
CHANGED
|
@@ -100,6 +100,17 @@ type RuntimeModelAuth = {
|
|
|
100
100
|
const MODEL_AUTH_PR_URL = "https://github.com/openclaw/openclaw/pull/41090";
|
|
101
101
|
const MODEL_AUTH_MERGE_COMMIT = "4790e40";
|
|
102
102
|
const MODEL_AUTH_REQUIRED_RELEASE = "the first OpenClaw release after 2026.3.8";
|
|
103
|
+
const AUTH_ERROR_TEXT_PATTERN =
|
|
104
|
+
/\b401\b|unauthorized|unauthorised|invalid[_ -]?token|invalid[_ -]?api[_ -]?key|authentication failed|authorization failed|missing scope|insufficient scope|model\.request\b/i;
|
|
105
|
+
const AUTH_ERROR_STATUS_KEYS = ["status", "statusCode", "status_code"] as const;
|
|
106
|
+
const AUTH_ERROR_NESTED_KEYS = ["error", "response", "cause", "details", "data", "body"] as const;
|
|
107
|
+
|
|
108
|
+
type CompletionBridgeErrorInfo = {
|
|
109
|
+
kind: "provider_auth";
|
|
110
|
+
statusCode?: number;
|
|
111
|
+
code?: string;
|
|
112
|
+
message?: string;
|
|
113
|
+
};
|
|
103
114
|
|
|
104
115
|
/** Capture plugin env values once during initialization. */
|
|
105
116
|
function snapshotPluginEnv(env: NodeJS.ProcessEnv = process.env): PluginEnvSnapshot {
|
|
@@ -115,6 +126,93 @@ function snapshotPluginEnv(env: NodeJS.ProcessEnv = process.env): PluginEnvSnaps
|
|
|
115
126
|
};
|
|
116
127
|
}
|
|
117
128
|
|
|
129
|
+
function truncateErrorMessage(message: string, maxChars = 240): string {
|
|
130
|
+
return message.length <= maxChars ? message : `${message.slice(0, maxChars)}...`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function collectErrorText(value: unknown, out: string[], depth = 0): void {
|
|
134
|
+
if (depth >= 4) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (typeof value === "string") {
|
|
138
|
+
const trimmed = value.trim();
|
|
139
|
+
if (trimmed) {
|
|
140
|
+
out.push(trimmed);
|
|
141
|
+
}
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (Array.isArray(value)) {
|
|
145
|
+
for (const entry of value.slice(0, 8)) {
|
|
146
|
+
collectErrorText(entry, out, depth + 1);
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (!isRecord(value)) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const entry of Object.values(value).slice(0, 12)) {
|
|
155
|
+
collectErrorText(entry, out, depth + 1);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function extractErrorStatusCode(value: unknown, depth = 0): number | undefined {
|
|
160
|
+
if (depth >= 4 || !isRecord(value)) {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (const key of AUTH_ERROR_STATUS_KEYS) {
|
|
165
|
+
const candidate = value[key];
|
|
166
|
+
if (typeof candidate === "number" && Number.isFinite(candidate)) {
|
|
167
|
+
return Math.trunc(candidate);
|
|
168
|
+
}
|
|
169
|
+
if (typeof candidate === "string") {
|
|
170
|
+
const parsed = Number.parseInt(candidate, 10);
|
|
171
|
+
if (Number.isFinite(parsed)) {
|
|
172
|
+
return parsed;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const key of AUTH_ERROR_NESTED_KEYS) {
|
|
178
|
+
const nested = value[key];
|
|
179
|
+
const statusCode = extractErrorStatusCode(nested, depth + 1);
|
|
180
|
+
if (statusCode !== undefined) {
|
|
181
|
+
return statusCode;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function detectProviderAuthError(error: unknown): CompletionBridgeErrorInfo | undefined {
|
|
189
|
+
const statusCode = extractErrorStatusCode(error);
|
|
190
|
+
const textParts: string[] = [];
|
|
191
|
+
collectErrorText(error, textParts);
|
|
192
|
+
const normalizedMessage = textParts.join(" ").replace(/\s+/g, " ").trim();
|
|
193
|
+
|
|
194
|
+
if (statusCode !== 401 && !AUTH_ERROR_TEXT_PATTERN.test(normalizedMessage)) {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const directCode =
|
|
199
|
+
isRecord(error) && typeof error.code === "string" && error.code.trim()
|
|
200
|
+
? error.code.trim()
|
|
201
|
+
: isRecord(error) &&
|
|
202
|
+
isRecord(error.error) &&
|
|
203
|
+
typeof error.error.code === "string" &&
|
|
204
|
+
error.error.code.trim()
|
|
205
|
+
? error.error.code.trim()
|
|
206
|
+
: undefined;
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
kind: "provider_auth",
|
|
210
|
+
...(statusCode !== undefined ? { statusCode } : {}),
|
|
211
|
+
...(directCode ? { code: directCode } : {}),
|
|
212
|
+
...(normalizedMessage ? { message: truncateErrorMessage(normalizedMessage) } : {}),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
118
216
|
/** Read OpenClaw's configured default model from the validated runtime config. */
|
|
119
217
|
function readDefaultModelFromConfig(config: unknown): string {
|
|
120
218
|
if (!config || typeof config !== "object") {
|
|
@@ -740,6 +838,53 @@ async function resolveApiKeyFromAuthProfiles(params: {
|
|
|
740
838
|
const expires = credential.expires;
|
|
741
839
|
const isExpired =
|
|
742
840
|
typeof expires === "number" && Number.isFinite(expires) && expires > 0 && Date.now() >= expires;
|
|
841
|
+
const shouldPreferOAuthHelper =
|
|
842
|
+
typeof params.piAiModule.getOAuthApiKey === "function" &&
|
|
843
|
+
normalizeProviderId(params.provider) === "openai-codex";
|
|
844
|
+
|
|
845
|
+
if (shouldPreferOAuthHelper) {
|
|
846
|
+
try {
|
|
847
|
+
const oauthCredential = {
|
|
848
|
+
access: credential.access ?? "",
|
|
849
|
+
refresh: credential.refresh ?? "",
|
|
850
|
+
expires: typeof credential.expires === "number" ? credential.expires : 0,
|
|
851
|
+
...(typeof credential.projectId === "string" ? { projectId: credential.projectId } : {}),
|
|
852
|
+
...(typeof credential.accountId === "string" ? { accountId: credential.accountId } : {}),
|
|
853
|
+
};
|
|
854
|
+
const refreshed = await params.piAiModule.getOAuthApiKey(params.provider, {
|
|
855
|
+
[params.provider]: oauthCredential,
|
|
856
|
+
});
|
|
857
|
+
if (refreshed?.apiKey) {
|
|
858
|
+
mergedStore.profiles[profileId] = {
|
|
859
|
+
...credential,
|
|
860
|
+
...refreshed.newCredentials,
|
|
861
|
+
type: "oauth",
|
|
862
|
+
};
|
|
863
|
+
if (persistPath) {
|
|
864
|
+
try {
|
|
865
|
+
writeFileSync(
|
|
866
|
+
persistPath,
|
|
867
|
+
JSON.stringify(
|
|
868
|
+
{
|
|
869
|
+
version: 1,
|
|
870
|
+
profiles: mergedStore.profiles,
|
|
871
|
+
...(mergedStore.order ? { order: mergedStore.order } : {}),
|
|
872
|
+
},
|
|
873
|
+
null,
|
|
874
|
+
2,
|
|
875
|
+
),
|
|
876
|
+
"utf8",
|
|
877
|
+
);
|
|
878
|
+
} catch {
|
|
879
|
+
// Ignore persistence errors: refreshed credentials remain usable in-memory for this run.
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return refreshed.apiKey;
|
|
883
|
+
}
|
|
884
|
+
} catch {
|
|
885
|
+
// Fall back to the cached access token below when helper resolution fails.
|
|
886
|
+
}
|
|
887
|
+
}
|
|
743
888
|
|
|
744
889
|
if (!isExpired && access) {
|
|
745
890
|
if (
|
|
@@ -1016,6 +1161,26 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
1016
1161
|
};
|
|
1017
1162
|
|
|
1018
1163
|
let resolvedApiKey = apiKey?.trim();
|
|
1164
|
+
if (!resolvedApiKey && modelAuth) {
|
|
1165
|
+
try {
|
|
1166
|
+
resolvedApiKey = resolveApiKeyFromAuthResult(
|
|
1167
|
+
await modelAuth.getApiKeyForModel({
|
|
1168
|
+
model: buildModelAuthLookupModel({
|
|
1169
|
+
provider: providerId,
|
|
1170
|
+
model: modelId,
|
|
1171
|
+
api: resolvedModel.api,
|
|
1172
|
+
}),
|
|
1173
|
+
cfg: api.config,
|
|
1174
|
+
...(authProfileId ? { profileId: authProfileId } : {}),
|
|
1175
|
+
}),
|
|
1176
|
+
);
|
|
1177
|
+
} catch (err) {
|
|
1178
|
+
console.error(
|
|
1179
|
+
`[lcm] modelAuth.getApiKeyForModel FAILED:`,
|
|
1180
|
+
err instanceof Error ? err.message : err,
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1019
1184
|
if (!resolvedApiKey && modelAuth) {
|
|
1020
1185
|
try {
|
|
1021
1186
|
resolvedApiKey = resolveApiKeyFromAuthResult(
|
|
@@ -1032,13 +1197,13 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
1032
1197
|
);
|
|
1033
1198
|
}
|
|
1034
1199
|
}
|
|
1035
|
-
if (!resolvedApiKey
|
|
1200
|
+
if (!resolvedApiKey) {
|
|
1036
1201
|
resolvedApiKey = resolveApiKey(providerId, readEnv);
|
|
1037
1202
|
}
|
|
1038
|
-
if (!resolvedApiKey &&
|
|
1203
|
+
if (!resolvedApiKey && typeof mod.getEnvApiKey === "function") {
|
|
1039
1204
|
resolvedApiKey = mod.getEnvApiKey(providerId)?.trim();
|
|
1040
1205
|
}
|
|
1041
|
-
if (!resolvedApiKey
|
|
1206
|
+
if (!resolvedApiKey) {
|
|
1042
1207
|
resolvedApiKey = await resolveApiKeyFromAuthProfiles({
|
|
1043
1208
|
provider: providerId,
|
|
1044
1209
|
authProfileId,
|
|
@@ -1072,6 +1237,19 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
1072
1237
|
temperature,
|
|
1073
1238
|
reasoning,
|
|
1074
1239
|
});
|
|
1240
|
+
const requestMetadata = {
|
|
1241
|
+
request_provider: providerId,
|
|
1242
|
+
request_model: modelId,
|
|
1243
|
+
request_api: resolvedModel.api,
|
|
1244
|
+
request_reasoning:
|
|
1245
|
+
typeof reasoning === "string" && reasoning.trim() ? reasoning.trim() : "(none)",
|
|
1246
|
+
request_has_system: typeof system === "string" && system.trim().length > 0 ? "true" : "false",
|
|
1247
|
+
request_temperature:
|
|
1248
|
+
typeof completeOptions.temperature === "number"
|
|
1249
|
+
? String(completeOptions.temperature)
|
|
1250
|
+
: "(omitted)",
|
|
1251
|
+
request_temperature_sent: typeof completeOptions.temperature === "number" ? "true" : "false",
|
|
1252
|
+
};
|
|
1075
1253
|
|
|
1076
1254
|
const result = await mod.completeSimple(
|
|
1077
1255
|
resolvedModel,
|
|
@@ -1091,40 +1269,22 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
1091
1269
|
if (!isRecord(result)) {
|
|
1092
1270
|
return {
|
|
1093
1271
|
content: [],
|
|
1094
|
-
|
|
1095
|
-
request_model: modelId,
|
|
1096
|
-
request_api: resolvedModel.api,
|
|
1097
|
-
request_reasoning:
|
|
1098
|
-
typeof reasoning === "string" && reasoning.trim() ? reasoning.trim() : "(none)",
|
|
1099
|
-
request_has_system:
|
|
1100
|
-
typeof system === "string" && system.trim().length > 0 ? "true" : "false",
|
|
1101
|
-
request_temperature:
|
|
1102
|
-
typeof completeOptions.temperature === "number"
|
|
1103
|
-
? String(completeOptions.temperature)
|
|
1104
|
-
: "(omitted)",
|
|
1105
|
-
request_temperature_sent:
|
|
1106
|
-
typeof completeOptions.temperature === "number" ? "true" : "false",
|
|
1272
|
+
...requestMetadata,
|
|
1107
1273
|
};
|
|
1108
1274
|
}
|
|
1109
1275
|
|
|
1110
1276
|
return {
|
|
1111
1277
|
...result,
|
|
1112
1278
|
content: Array.isArray(result.content) ? result.content : [],
|
|
1113
|
-
|
|
1114
|
-
request_model: modelId,
|
|
1115
|
-
request_api: resolvedModel.api,
|
|
1116
|
-
request_reasoning:
|
|
1117
|
-
typeof reasoning === "string" && reasoning.trim() ? reasoning.trim() : "(none)",
|
|
1118
|
-
request_has_system: typeof system === "string" && system.trim().length > 0 ? "true" : "false",
|
|
1119
|
-
request_temperature:
|
|
1120
|
-
typeof completeOptions.temperature === "number"
|
|
1121
|
-
? String(completeOptions.temperature)
|
|
1122
|
-
: "(omitted)",
|
|
1123
|
-
request_temperature_sent: typeof completeOptions.temperature === "number" ? "true" : "false",
|
|
1279
|
+
...requestMetadata,
|
|
1124
1280
|
};
|
|
1125
1281
|
} catch (err) {
|
|
1126
1282
|
console.error(`[lcm] completeSimple error:`, err instanceof Error ? err.message : err);
|
|
1127
|
-
|
|
1283
|
+
const authError = detectProviderAuthError(err);
|
|
1284
|
+
return {
|
|
1285
|
+
content: [],
|
|
1286
|
+
...(authError ? { error: authError } : {}),
|
|
1287
|
+
};
|
|
1128
1288
|
}
|
|
1129
1289
|
},
|
|
1130
1290
|
callGateway: async (params) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { DatabaseSync } from "node:sqlite";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { sanitizeFts5Query } from "./fts5-sanitize.js";
|
|
4
|
-
import { buildLikeSearchPlan, createFallbackSnippet } from "./full-text-fallback.js";
|
|
4
|
+
import { buildLikeSearchPlan, containsCjk, createFallbackSnippet } from "./full-text-fallback.js";
|
|
5
5
|
|
|
6
6
|
export type ConversationId = number;
|
|
7
7
|
export type MessageId = number;
|
|
@@ -205,6 +205,47 @@ function toMessagePartRecord(row: MessagePartRow): MessagePartRecord {
|
|
|
205
205
|
};
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
function normalizeMessageContentForFullTextIndex(content: string): string | null {
|
|
209
|
+
const trimmed = content.trim();
|
|
210
|
+
if (!trimmed) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const isExternalizedReference =
|
|
215
|
+
trimmed.startsWith("[LCM File:") || trimmed.startsWith("[LCM Tool Output:");
|
|
216
|
+
if (!isExternalizedReference) {
|
|
217
|
+
return content;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const lines = trimmed
|
|
221
|
+
.split(/\r?\n/)
|
|
222
|
+
.map((line) => line.trim())
|
|
223
|
+
.filter((line) => line.length > 0);
|
|
224
|
+
if (lines.length === 0) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const header = lines[0] ?? "";
|
|
229
|
+
const summaryLines: string[] = [];
|
|
230
|
+
let inSummary = false;
|
|
231
|
+
for (let index = 1; index < lines.length; index += 1) {
|
|
232
|
+
const line = lines[index]!;
|
|
233
|
+
if (line === "Exploration Summary:") {
|
|
234
|
+
inSummary = true;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (line.startsWith("Use lcm_describe")) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (inSummary) {
|
|
241
|
+
summaryLines.push(line);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const normalized = [header, ...summaryLines].filter((line) => line.length > 0).join("\n");
|
|
246
|
+
return normalized || null;
|
|
247
|
+
}
|
|
248
|
+
|
|
208
249
|
// ── ConversationStore ─────────────────────────────────────────────────────────
|
|
209
250
|
|
|
210
251
|
export class ConversationStore {
|
|
@@ -622,6 +663,17 @@ export class ConversationStore {
|
|
|
622
663
|
const limit = input.limit ?? 50;
|
|
623
664
|
|
|
624
665
|
if (input.mode === "full_text") {
|
|
666
|
+
// FTS5 unicode61 can return incomplete matches for CJK text, so route
|
|
667
|
+
// those queries through the existing LIKE fallback path immediately.
|
|
668
|
+
if (containsCjk(input.query)) {
|
|
669
|
+
return this.searchLike(
|
|
670
|
+
input.query,
|
|
671
|
+
limit,
|
|
672
|
+
input.conversationId,
|
|
673
|
+
input.since,
|
|
674
|
+
input.before,
|
|
675
|
+
);
|
|
676
|
+
}
|
|
625
677
|
if (this.fts5Available) {
|
|
626
678
|
try {
|
|
627
679
|
return this.searchFullText(
|
|
@@ -650,10 +702,14 @@ export class ConversationStore {
|
|
|
650
702
|
if (!this.fts5Available) {
|
|
651
703
|
return;
|
|
652
704
|
}
|
|
705
|
+
const normalizedContent = normalizeMessageContentForFullTextIndex(content);
|
|
706
|
+
if (!normalizedContent) {
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
653
709
|
try {
|
|
654
710
|
this.db
|
|
655
711
|
.prepare(`INSERT INTO messages_fts(rowid, content) VALUES (?, ?)`)
|
|
656
|
-
.run(messageId,
|
|
712
|
+
.run(messageId, normalizedContent);
|
|
657
713
|
} catch {
|
|
658
714
|
// Full-text indexing is optional. Message persistence must still succeed.
|
|
659
715
|
}
|
|
@@ -748,14 +804,24 @@ export class ConversationStore {
|
|
|
748
804
|
)
|
|
749
805
|
.all(...args) as unknown as MessageRow[];
|
|
750
806
|
|
|
751
|
-
return rows
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
807
|
+
return rows
|
|
808
|
+
.map((row) => {
|
|
809
|
+
const normalizedContent = normalizeMessageContentForFullTextIndex(row.content) ?? row.content;
|
|
810
|
+
const haystack = normalizedContent.toLowerCase();
|
|
811
|
+
const matchesAllTerms = plan.terms.every((term) => haystack.includes(term));
|
|
812
|
+
if (!matchesAllTerms) {
|
|
813
|
+
return null;
|
|
814
|
+
}
|
|
815
|
+
return {
|
|
816
|
+
messageId: row.message_id,
|
|
817
|
+
conversationId: row.conversation_id,
|
|
818
|
+
role: row.role,
|
|
819
|
+
snippet: createFallbackSnippet(normalizedContent, plan.terms),
|
|
820
|
+
createdAt: new Date(row.created_at),
|
|
821
|
+
rank: 0,
|
|
822
|
+
};
|
|
823
|
+
})
|
|
824
|
+
.filter((row): row is MessageSearchResult => row !== null);
|
|
759
825
|
}
|
|
760
826
|
|
|
761
827
|
private searchRegex(
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
const RAW_TERM_RE = /"([^"]+)"|(\S+)/g;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Detect whether a query contains CJK characters that FTS5 unicode61
|
|
5
|
+
* tokenizer cannot index properly.
|
|
6
|
+
*/
|
|
7
|
+
const CJK_RE = /[\u2E80-\u9FFF\u3400-\u4DBF\uF900-\uFAFF\uAC00-\uD7AF\u3040-\u309F\u30A0-\u30FF]/;
|
|
8
|
+
export function containsCjk(text: string): boolean {
|
|
9
|
+
return CJK_RE.test(text);
|
|
10
|
+
}
|
|
2
11
|
const EDGE_PUNCTUATION_RE = /^[`'"()[\]{}<>.,:;!?*_+=|\\/-]+|[`'"()[\]{}<>.,:;!?*_+=|\\/-]+$/g;
|
|
3
12
|
|
|
4
13
|
export type LikeSearchPlan = {
|