jinzd-ai-cli 0.4.232 → 0.4.234
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{batch-D6XYKHHA.js → batch-T3FRX4B6.js} +2 -2
- package/dist/{chat-index-O56HDGFI.js → chat-index-7HXBWQFH.js} +2 -2
- package/dist/{chat-index-WSI7ULRQ.js → chat-index-WYXYD7YP.js} +1 -1
- package/dist/{chunk-4KM53GLG.js → chunk-2QQKS56X.js} +1 -1
- package/dist/{chunk-EPDW73H7.js → chunk-3KTSJ2EL.js} +1 -1
- package/dist/{chunk-KIQ2ICMV.js → chunk-5DTEEWYC.js} +4 -4
- package/dist/{chunk-NVUCDUXE.js → chunk-5MYPIQ3Y.js} +11 -5
- package/dist/{chunk-YDGKT6RI.js → chunk-5VPRZMZB.js} +2 -2
- package/dist/{chunk-RRMLRCPW.js → chunk-6MJMHHRK.js} +4 -4
- package/dist/{chunk-2FZACYUE.js → chunk-AFT6VFMI.js} +1 -1
- package/dist/{chunk-TJVM535A.js → chunk-E5RLXMSU.js} +6 -6
- package/dist/{chunk-XVR4YKRC.js → chunk-ETJ5X6V7.js} +1 -1
- package/dist/{chunk-6VSAKQTN.js → chunk-II6RZRTF.js} +1 -1
- package/dist/{chunk-ZWU7FWHO.js → chunk-JMP3LJC2.js} +38 -91
- package/dist/chunk-KCEO2XJ4.js +420 -0
- package/dist/{chunk-LKTYTDEJ.js → chunk-L5PNAQ6O.js} +1 -1
- package/dist/{chunk-ODYTXQ26.js → chunk-O3XSFPYH.js} +35 -3
- package/dist/{chunk-62R7CACG.js → chunk-QYQI7ZWK.js} +1 -1
- package/dist/{chunk-2HWL45PG.js → chunk-TAXIG7HH.js} +1 -1
- package/dist/{chunk-LF3OUJHK.js → chunk-UO3XWZFE.js} +302 -4
- package/dist/{ci-OR57Z7QF.js → ci-GJ53GPLA.js} +6 -6
- package/dist/{ci-format-DMMS2ZFK.js → ci-format-AXE4QCI6.js} +2 -2
- package/dist/{constants-7GZKINR5.js → constants-DC4EXFSB.js} +1 -1
- package/dist/{doctor-cli-ZD37GAZD.js → doctor-cli-HKREND6Q.js} +6 -6
- package/dist/electron-server.js +78 -99
- package/dist/{hub-VFYFUSM6.js → hub-LTTKRGZC.js} +2 -2
- package/dist/index.js +24 -24
- package/dist/{persist-I3JI27OK.js → persist-AMH5SQ4W.js} +3 -3
- package/dist/{pr-YF6TXJYE.js → pr-4VODJVRV.js} +6 -6
- package/dist/{run-tests-HEDDC6DJ.js → run-tests-G5BNTMWQ.js} +2 -2
- package/dist/{run-tests-BNZZDCTU.js → run-tests-QR2PYR2P.js} +1 -1
- package/dist/{server-HCDI2EID.js → server-64WZVZZN.js} +17 -17
- package/dist/{server-C52P6IDO.js → server-EO6VQHKL.js} +7 -7
- package/dist/{task-orchestrator-QIEHYRGK.js → task-orchestrator-4INCZMLT.js} +7 -7
- package/dist/{usage-4AAMJBBM.js → usage-T3HFMNNV.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-B5TYJO7V.js +0 -123
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
atomicWriteFileSync
|
|
4
|
+
} from "./chunk-IW3Q7AE5.js";
|
|
5
|
+
|
|
6
|
+
// src/security/redactor.ts
|
|
7
|
+
var DEFAULT_PATTERNS = [
|
|
8
|
+
// password: xxx / password = xxx / password="xxx"
|
|
9
|
+
// Covers YAML / JSON / shell-ish / env-file forms.
|
|
10
|
+
{ kind: "password", regex: /\b(password|passwd|pwd)\s*[:=]\s*["']?([^\s"',;{}]{4,200})["']?/gi },
|
|
11
|
+
// PGPASSWORD=xxx (explicit bash env-var form, separate rule because no quotes usually)
|
|
12
|
+
{ kind: "pgpassword-env", regex: /\b(PGPASSWORD)=([^\s"']{4,200})/g },
|
|
13
|
+
// JDBC/PG/MySQL/Mongo connection strings with inline credentials
|
|
14
|
+
// postgresql://user:pass@host/db → redact pass
|
|
15
|
+
{ kind: "db-uri-password", regex: /(\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp|mssql):\/\/[^:\s]+:)([^@\s]+)(@)/gi },
|
|
16
|
+
// Anthropic API keys
|
|
17
|
+
{ kind: "anthropic-key", regex: /(sk-ant-[a-zA-Z0-9_-]{90,})/g },
|
|
18
|
+
// L6 (v0.4.108): Zhipu / GLM API keys — `<24+ hex/base64-ish>.<32+>`
|
|
19
|
+
// Two segments separated by a dot, each safely identifiable by length
|
|
20
|
+
// and char class. Conservative on the lower bound so we don't eat
|
|
21
|
+
// version strings like `1.0.0` or filenames.
|
|
22
|
+
{ kind: "zhipu-key", regex: /\b([a-zA-Z0-9]{24,}\.[a-zA-Z0-9]{32,})\b/g },
|
|
23
|
+
// OpenAI / generic sk- keys — requires length ≥32 to avoid eating short identifiers
|
|
24
|
+
{ kind: "openai-key", regex: /(sk-(?:proj-)?[a-zA-Z0-9_-]{32,})/g },
|
|
25
|
+
// GitHub personal access tokens
|
|
26
|
+
{ kind: "github-pat", regex: /\b(ghp_[a-zA-Z0-9]{36})\b/g },
|
|
27
|
+
{ kind: "github-oauth", regex: /\b(gho_[a-zA-Z0-9]{36})\b/g },
|
|
28
|
+
{ kind: "github-install", regex: /\b(ghs_[a-zA-Z0-9]{36})\b/g },
|
|
29
|
+
// Slack tokens
|
|
30
|
+
{ kind: "slack-bot", regex: /\b(xoxb-\d+-\d+-[a-zA-Z0-9]+)\b/g },
|
|
31
|
+
{ kind: "slack-user", regex: /\b(xoxp-\d+-\d+-\d+-[a-zA-Z0-9]+)\b/g },
|
|
32
|
+
// AWS access key IDs (AKIA...) and secret access keys are context-dependent;
|
|
33
|
+
// we only catch the ID because secret key alone is indistinguishable from random base64.
|
|
34
|
+
{ kind: "aws-access-key-id", regex: /\b(AKIA[0-9A-Z]{16})\b/g },
|
|
35
|
+
// Google API keys
|
|
36
|
+
{ kind: "google-api-key", regex: /\b(AIza[0-9A-Za-z_-]{35})\b/g },
|
|
37
|
+
// Generic "api_key": "..." / "apiKey": "..." / api-key=xxx
|
|
38
|
+
{ kind: "api-key", regex: /\b(api[_-]?key)\s*[:=]\s*["']?([a-zA-Z0-9_\-.]{16,200})["']?/gi },
|
|
39
|
+
// Generic token: xxx (only when value looks token-shaped; avoids eating human prose)
|
|
40
|
+
{ kind: "token", regex: /\b(token|access[_-]?token|bearer[_-]?token)\s*[:=]\s*["']?([a-zA-Z0-9_\-.]{20,300})["']?/gi },
|
|
41
|
+
// Bearer <token> in Authorization headers
|
|
42
|
+
{ kind: "bearer", regex: /\b(Authorization:\s*Bearer\s+)([a-zA-Z0-9_\-.=]{20,500})/g },
|
|
43
|
+
// Private key PEM blocks — catch the header+footer together
|
|
44
|
+
// P2-PERF-02: PEM bodies use strict Base64 + =\n — narrowed char class avoids backtracking
|
|
45
|
+
{ kind: "private-key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[ \t\n\r]*[A-Za-z0-9+/=\n\r-]*[ \t\n\r]*-----END [A-Z ]*PRIVATE KEY-----/g }
|
|
46
|
+
];
|
|
47
|
+
var MAX_CUSTOM = 32;
|
|
48
|
+
var MAX_PATTERN_LEN = 500;
|
|
49
|
+
var SUSPICIOUS_REDOS = /\([^)]*[+*][^)]*\)\s*[+*{]/;
|
|
50
|
+
var MAX_REDACT_INPUT = 512e3;
|
|
51
|
+
function render(placeholder, kind) {
|
|
52
|
+
return placeholder.replace("{kind}", kind);
|
|
53
|
+
}
|
|
54
|
+
function redactString(input, options) {
|
|
55
|
+
if (!options.enabled || !input) return { redacted: input, hits: [] };
|
|
56
|
+
if (input.length > MAX_REDACT_INPUT) return { redacted: input, hits: [] };
|
|
57
|
+
const placeholder = options.placeholder ?? "[REDACTED:{kind}]";
|
|
58
|
+
const customSrcs = (options.customRegexes ?? []).slice(0, MAX_CUSTOM);
|
|
59
|
+
const patterns = [
|
|
60
|
+
...options.patterns ?? DEFAULT_PATTERNS,
|
|
61
|
+
...customSrcs.flatMap((src, i) => {
|
|
62
|
+
if (typeof src !== "string" || src.length === 0 || src.length > MAX_PATTERN_LEN) return [];
|
|
63
|
+
try {
|
|
64
|
+
const flags = src.match(/^\/.*\/([gimsuy]*)$/)?.[1] ?? "";
|
|
65
|
+
const body = src.replace(/^\/(.*)\/[gimsuy]*$/, "$1");
|
|
66
|
+
if (SUSPICIOUS_REDOS.test(body)) return [];
|
|
67
|
+
const regex = new RegExp(body, flags.includes("g") ? flags : flags + "g");
|
|
68
|
+
return [{ kind: `custom-${i}`, regex }];
|
|
69
|
+
} catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
];
|
|
74
|
+
let redacted = input;
|
|
75
|
+
const hits = [];
|
|
76
|
+
for (const { kind, regex } of patterns) {
|
|
77
|
+
const rx = new RegExp(regex.source, regex.flags);
|
|
78
|
+
const captureCount = new RegExp(rx.source + "|").exec("").length - 1;
|
|
79
|
+
redacted = redacted.replace(rx, (...args) => {
|
|
80
|
+
const match = args[0];
|
|
81
|
+
const g1 = captureCount >= 1 ? args[1] : void 0;
|
|
82
|
+
const g2 = captureCount >= 2 ? args[2] : void 0;
|
|
83
|
+
const offset = args[1 + captureCount];
|
|
84
|
+
if (captureCount >= 2 && typeof g2 === "string") {
|
|
85
|
+
hits.push({ kind, start: offset + (g1?.length ?? 0), length: g2.length, secret: g2 });
|
|
86
|
+
return `${g1}${render(placeholder, kind)}`;
|
|
87
|
+
}
|
|
88
|
+
hits.push({ kind, start: offset, length: match.length, secret: g1 ?? match });
|
|
89
|
+
return render(placeholder, kind);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return { redacted, hits };
|
|
93
|
+
}
|
|
94
|
+
function redactJson(value, options) {
|
|
95
|
+
if (!options.enabled) return { value, hits: [] };
|
|
96
|
+
const allHits = [];
|
|
97
|
+
function walk(v) {
|
|
98
|
+
if (typeof v === "string") {
|
|
99
|
+
const r = redactString(v, options);
|
|
100
|
+
allHits.push(...r.hits);
|
|
101
|
+
return r.redacted;
|
|
102
|
+
}
|
|
103
|
+
if (Array.isArray(v)) return v.map(walk);
|
|
104
|
+
if (v && typeof v === "object") {
|
|
105
|
+
const out = {};
|
|
106
|
+
for (const [k, vv] of Object.entries(v)) {
|
|
107
|
+
out[k] = walk(vv);
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
return v;
|
|
112
|
+
}
|
|
113
|
+
const redacted = walk(value);
|
|
114
|
+
return { value: redacted, hits: allHits };
|
|
115
|
+
}
|
|
116
|
+
function scanString(input, options) {
|
|
117
|
+
const { hits } = redactString(input, { ...options, enabled: true });
|
|
118
|
+
return hits;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/session/session-index.ts
|
|
122
|
+
import { existsSync, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
|
|
123
|
+
import { join } from "path";
|
|
124
|
+
|
|
125
|
+
// src/session/session-meta.ts
|
|
126
|
+
import { readFileSync, openSync, readSync, closeSync } from "fs";
|
|
127
|
+
function safeDate(value) {
|
|
128
|
+
const d = new Date(value);
|
|
129
|
+
return isNaN(d.getTime()) ? /* @__PURE__ */ new Date(0) : d;
|
|
130
|
+
}
|
|
131
|
+
function extractJsonField(header, field) {
|
|
132
|
+
const re = new RegExp(`"${field}"\\s*:\\s*"([^"]*)"`, "i");
|
|
133
|
+
const m = header.match(re);
|
|
134
|
+
return m ? m[1] : void 0;
|
|
135
|
+
}
|
|
136
|
+
function extractJsonNumberField(header, field) {
|
|
137
|
+
const m = header.match(new RegExp(`"${field}"\\s*:\\s*(\\d+)`));
|
|
138
|
+
return m ? Number(m[1]) : void 0;
|
|
139
|
+
}
|
|
140
|
+
function readSessionMeta(filePath) {
|
|
141
|
+
const HEADER_SIZE = 1024;
|
|
142
|
+
let header;
|
|
143
|
+
try {
|
|
144
|
+
const fd = openSync(filePath, "r");
|
|
145
|
+
const buf = Buffer.alloc(HEADER_SIZE);
|
|
146
|
+
const bytesRead = readSync(fd, buf, 0, HEADER_SIZE, 0);
|
|
147
|
+
closeSync(fd);
|
|
148
|
+
header = buf.toString("utf-8", 0, bytesRead);
|
|
149
|
+
} catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
const id = extractJsonField(header, "id");
|
|
153
|
+
const provider = extractJsonField(header, "provider");
|
|
154
|
+
const model = extractJsonField(header, "model");
|
|
155
|
+
const created = extractJsonField(header, "created");
|
|
156
|
+
const updated = extractJsonField(header, "updated");
|
|
157
|
+
const title = extractJsonField(header, "title");
|
|
158
|
+
if (id && provider && model) {
|
|
159
|
+
let messageCount = extractJsonNumberField(header, "messageCount");
|
|
160
|
+
if (messageCount === void 0) {
|
|
161
|
+
messageCount = 0;
|
|
162
|
+
try {
|
|
163
|
+
const full = readFileSync(filePath, "utf-8");
|
|
164
|
+
const matches = full.match(/"role"\s*:/g);
|
|
165
|
+
messageCount = matches ? matches.length : 0;
|
|
166
|
+
} catch {
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
let branchCount;
|
|
170
|
+
let activeBranch;
|
|
171
|
+
try {
|
|
172
|
+
const brMatch = header.match(/"branches"\s*:\s*\[/);
|
|
173
|
+
if (brMatch) {
|
|
174
|
+
const parentMatches = header.match(/"parentBranchId"/g);
|
|
175
|
+
branchCount = parentMatches ? parentMatches.length + 1 : 1;
|
|
176
|
+
}
|
|
177
|
+
activeBranch = extractJsonField(header, "activeBranchId");
|
|
178
|
+
} catch {
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
id,
|
|
182
|
+
provider,
|
|
183
|
+
model,
|
|
184
|
+
messageCount,
|
|
185
|
+
created: safeDate(created),
|
|
186
|
+
updated: safeDate(updated),
|
|
187
|
+
title: title || void 0,
|
|
188
|
+
branchCount,
|
|
189
|
+
activeBranch
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const data = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
193
|
+
return {
|
|
194
|
+
id: data.id,
|
|
195
|
+
provider: data.provider,
|
|
196
|
+
model: data.model,
|
|
197
|
+
messageCount: data.messages?.length ?? 0,
|
|
198
|
+
created: safeDate(data.created),
|
|
199
|
+
updated: safeDate(data.updated),
|
|
200
|
+
title: data.title,
|
|
201
|
+
branchCount: data.branches?.length ?? 1,
|
|
202
|
+
activeBranch: data.activeBranchId
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/session/session-index.ts
|
|
207
|
+
var INDEX_FILE = "sessions.index.json";
|
|
208
|
+
function indexFilePath(historyDir) {
|
|
209
|
+
return join(historyDir, INDEX_FILE);
|
|
210
|
+
}
|
|
211
|
+
function loadIndex(historyDir) {
|
|
212
|
+
const fp = indexFilePath(historyDir);
|
|
213
|
+
if (!existsSync(fp)) return null;
|
|
214
|
+
try {
|
|
215
|
+
const data = JSON.parse(readFileSync2(fp, "utf-8"));
|
|
216
|
+
if (data?.version === 1 && data.sessions && data.invertedIndex) {
|
|
217
|
+
return data;
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
} catch {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function saveIndex(historyDir, idx) {
|
|
225
|
+
idx.built = (/* @__PURE__ */ new Date()).toISOString();
|
|
226
|
+
const fp = indexFilePath(historyDir);
|
|
227
|
+
atomicWriteFileSync(fp, JSON.stringify(idx));
|
|
228
|
+
}
|
|
229
|
+
function rebuildIndex(historyDir) {
|
|
230
|
+
if (!existsSync(historyDir)) {
|
|
231
|
+
const empty = { version: 1, built: (/* @__PURE__ */ new Date()).toISOString(), sessions: {}, invertedIndex: {} };
|
|
232
|
+
return empty;
|
|
233
|
+
}
|
|
234
|
+
const files = readdirSync(historyDir).filter((f) => f.endsWith(".json"));
|
|
235
|
+
const sessions = {};
|
|
236
|
+
const invertedIndex = /* @__PURE__ */ new Map();
|
|
237
|
+
for (const file of files) {
|
|
238
|
+
const filePath = join(historyDir, file);
|
|
239
|
+
const meta = readSessionMeta(filePath);
|
|
240
|
+
if (!meta) continue;
|
|
241
|
+
let fileSize = 0;
|
|
242
|
+
try {
|
|
243
|
+
fileSize = statSync(filePath).size;
|
|
244
|
+
} catch {
|
|
245
|
+
}
|
|
246
|
+
let mtimeMs = 0;
|
|
247
|
+
try {
|
|
248
|
+
mtimeMs = statSync(filePath).mtimeMs;
|
|
249
|
+
} catch {
|
|
250
|
+
}
|
|
251
|
+
const entry = {
|
|
252
|
+
id: meta.id,
|
|
253
|
+
provider: meta.provider,
|
|
254
|
+
model: meta.model,
|
|
255
|
+
title: meta.title,
|
|
256
|
+
messageCount: meta.messageCount,
|
|
257
|
+
fileSize,
|
|
258
|
+
created: meta.created.toISOString(),
|
|
259
|
+
updated: meta.updated.toISOString(),
|
|
260
|
+
branchCount: meta.branchCount,
|
|
261
|
+
activeBranch: meta.activeBranch,
|
|
262
|
+
_mtimeMs: mtimeMs
|
|
263
|
+
};
|
|
264
|
+
sessions[meta.id] = entry;
|
|
265
|
+
addTokensToIndex(invertedIndex, meta.title ?? "", meta.id);
|
|
266
|
+
}
|
|
267
|
+
const serializedIndex = {};
|
|
268
|
+
for (const [token, ids] of invertedIndex) {
|
|
269
|
+
serializedIndex[token] = [...ids];
|
|
270
|
+
}
|
|
271
|
+
const idx = {
|
|
272
|
+
version: 1,
|
|
273
|
+
built: (/* @__PURE__ */ new Date()).toISOString(),
|
|
274
|
+
sessions,
|
|
275
|
+
invertedIndex: serializedIndex
|
|
276
|
+
};
|
|
277
|
+
saveIndex(historyDir, idx);
|
|
278
|
+
return idx;
|
|
279
|
+
}
|
|
280
|
+
function updateSessionInIndex(historyDir, meta, filePath) {
|
|
281
|
+
let idx = loadIndex(historyDir);
|
|
282
|
+
if (!idx) {
|
|
283
|
+
rebuildIndex(historyDir);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
let fileSize = 0;
|
|
287
|
+
try {
|
|
288
|
+
fileSize = statSync(filePath).size;
|
|
289
|
+
} catch {
|
|
290
|
+
}
|
|
291
|
+
let mtimeMs = 0;
|
|
292
|
+
try {
|
|
293
|
+
mtimeMs = statSync(filePath).mtimeMs;
|
|
294
|
+
} catch {
|
|
295
|
+
}
|
|
296
|
+
const id = meta.id;
|
|
297
|
+
for (const [token, ids] of Object.entries(idx.invertedIndex)) {
|
|
298
|
+
const filtered = ids.filter((sid) => sid !== id);
|
|
299
|
+
if (filtered.length === 0) {
|
|
300
|
+
delete idx.invertedIndex[token];
|
|
301
|
+
} else {
|
|
302
|
+
idx.invertedIndex[token] = filtered;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
idx.sessions[id] = {
|
|
306
|
+
id,
|
|
307
|
+
provider: meta.provider,
|
|
308
|
+
model: meta.model,
|
|
309
|
+
title: meta.title,
|
|
310
|
+
messageCount: meta.messageCount,
|
|
311
|
+
fileSize,
|
|
312
|
+
created: meta.created.toISOString(),
|
|
313
|
+
updated: meta.updated.toISOString(),
|
|
314
|
+
_mtimeMs: mtimeMs
|
|
315
|
+
};
|
|
316
|
+
const tokens = tokenize(meta.title ?? "");
|
|
317
|
+
for (const token of tokens) {
|
|
318
|
+
const existing = idx.invertedIndex[token] ?? [];
|
|
319
|
+
if (!existing.includes(id)) {
|
|
320
|
+
existing.push(id);
|
|
321
|
+
idx.invertedIndex[token] = existing;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
saveIndex(historyDir, idx);
|
|
325
|
+
}
|
|
326
|
+
function removeSessionFromIndex(historyDir, id) {
|
|
327
|
+
const idx = loadIndex(historyDir);
|
|
328
|
+
if (!idx) return;
|
|
329
|
+
delete idx.sessions[id];
|
|
330
|
+
for (const [token, ids] of Object.entries(idx.invertedIndex)) {
|
|
331
|
+
const filtered = ids.filter((sid) => sid !== id);
|
|
332
|
+
if (filtered.length === 0) {
|
|
333
|
+
delete idx.invertedIndex[token];
|
|
334
|
+
} else {
|
|
335
|
+
idx.invertedIndex[token] = filtered;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
saveIndex(historyDir, idx);
|
|
339
|
+
}
|
|
340
|
+
function listFromIndex(historyDir, opts) {
|
|
341
|
+
let idx = loadIndex(historyDir);
|
|
342
|
+
if (!idx) {
|
|
343
|
+
idx = rebuildIndex(historyDir);
|
|
344
|
+
}
|
|
345
|
+
return Object.values(idx.sessions).map((entry) => ({
|
|
346
|
+
id: entry.id,
|
|
347
|
+
provider: entry.provider,
|
|
348
|
+
model: entry.model,
|
|
349
|
+
title: entry.title,
|
|
350
|
+
messageCount: entry.messageCount,
|
|
351
|
+
created: new Date(entry.created),
|
|
352
|
+
updated: new Date(entry.updated),
|
|
353
|
+
fileSize: opts?.includeFileSize ? entry.fileSize : void 0,
|
|
354
|
+
branchCount: entry.branchCount,
|
|
355
|
+
activeBranch: entry.activeBranch
|
|
356
|
+
})).sort((a, b) => b.updated.getTime() - a.updated.getTime());
|
|
357
|
+
}
|
|
358
|
+
function searchCandidates(historyDir, query) {
|
|
359
|
+
const idx = loadIndex(historyDir);
|
|
360
|
+
if (!idx) return null;
|
|
361
|
+
const queryTokens = tokenize(query.toLowerCase());
|
|
362
|
+
if (queryTokens.length === 0) return null;
|
|
363
|
+
const scoreMap = /* @__PURE__ */ new Map();
|
|
364
|
+
for (const token of queryTokens) {
|
|
365
|
+
const matchingIds = idx.invertedIndex[token];
|
|
366
|
+
if (!matchingIds) continue;
|
|
367
|
+
for (const sid of matchingIds) {
|
|
368
|
+
scoreMap.set(sid, (scoreMap.get(sid) ?? 0) + 1);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const candidates = [...scoreMap.entries()].filter(([, score]) => score > 0).sort((a, b) => {
|
|
372
|
+
const scoreDiff = b[1] - a[1];
|
|
373
|
+
if (scoreDiff !== 0) return scoreDiff;
|
|
374
|
+
const aMeta = idx.sessions[a[0]];
|
|
375
|
+
const bMeta = idx.sessions[b[0]];
|
|
376
|
+
return (bMeta?.updated ?? "").localeCompare(aMeta?.updated ?? "");
|
|
377
|
+
}).map(([sid]) => sid);
|
|
378
|
+
return candidates.length > 0 ? candidates : null;
|
|
379
|
+
}
|
|
380
|
+
function getIndexMtimeMap(historyDir) {
|
|
381
|
+
const idx = loadIndex(historyDir);
|
|
382
|
+
if (!idx) return null;
|
|
383
|
+
const map = {};
|
|
384
|
+
for (const entry of Object.values(idx.sessions)) {
|
|
385
|
+
map[entry.id] = entry._mtimeMs;
|
|
386
|
+
}
|
|
387
|
+
return map;
|
|
388
|
+
}
|
|
389
|
+
function addTokensToIndex(invertedIndex, text, sessionId) {
|
|
390
|
+
for (const token of tokenize(text)) {
|
|
391
|
+
const s = invertedIndex.get(token) ?? /* @__PURE__ */ new Set();
|
|
392
|
+
s.add(sessionId);
|
|
393
|
+
invertedIndex.set(token, s);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function tokenize(text) {
|
|
397
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
398
|
+
const cjkOnly = text.replace(/[^\u4e00-\u9fff\u3400-\u4dbf]/g, "");
|
|
399
|
+
for (let i = 0; i < cjkOnly.length - 1; i++) {
|
|
400
|
+
tokens.add(cjkOnly.slice(i, i + 2));
|
|
401
|
+
}
|
|
402
|
+
const english = text.replace(/[\u4e00-\u9fff\u3400-\u4dbf]/g, " ").toLowerCase();
|
|
403
|
+
const words = english.split(/[^a-z0-9]+/).filter((w) => w.length >= 2);
|
|
404
|
+
for (const w of words) tokens.add(w);
|
|
405
|
+
return [...tokens];
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export {
|
|
409
|
+
DEFAULT_PATTERNS,
|
|
410
|
+
redactString,
|
|
411
|
+
redactJson,
|
|
412
|
+
scanString,
|
|
413
|
+
safeDate,
|
|
414
|
+
readSessionMeta,
|
|
415
|
+
updateSessionInIndex,
|
|
416
|
+
removeSessionFromIndex,
|
|
417
|
+
listFromIndex,
|
|
418
|
+
searchCandidates,
|
|
419
|
+
getIndexMtimeMap
|
|
420
|
+
};
|
|
@@ -466,7 +466,10 @@ function peelMetaNarration(content) {
|
|
|
466
466
|
var META_NARRATION_HARD_MARKERS = [
|
|
467
467
|
/\[⚠️\s*CONTENT GENERATION MODE\]/,
|
|
468
468
|
/CONTENT_ONLY_STREAM_REMINDER\b/,
|
|
469
|
-
/<system-reminder>/i
|
|
469
|
+
/<system-reminder>/i,
|
|
470
|
+
// → `path` — AI wraps content with "已写好 → `file.md`" before the document body.
|
|
471
|
+
// Documents never use this conversational file-path-pointer syntax.
|
|
472
|
+
/→\s*`[^`]{2,}`/u
|
|
470
473
|
];
|
|
471
474
|
var META_NARRATION_HEURISTICS = [
|
|
472
475
|
/\bthe user (?:is asking me|wants me|is requesting|expects me)\b/i,
|
|
@@ -477,7 +480,24 @@ var META_NARRATION_HEURISTICS = [
|
|
|
477
480
|
/\bActually,?\s+I\b/i,
|
|
478
481
|
/\bI need to be honest with the user\b/i,
|
|
479
482
|
/\bI(?:'m| am) in a special mode\b/i,
|
|
480
|
-
/\bGiven that I cannot\b/i
|
|
483
|
+
/\bGiven that I cannot\b/i,
|
|
484
|
+
// ── 中文 ── (need 2+ total hits across all patterns to trigger)
|
|
485
|
+
/(?:已经?|已)\s*(?:写[好完]|生成[好完]|创建[好完]|准备[好完]|搞[好定]|弄[好完])[了啦]?/u,
|
|
486
|
+
/(?:需要你|请你|麻烦你|还要你|你看)\s*(?:确认|决定|选择|帮忙|看[一看下]|过目)/u,
|
|
487
|
+
/(?:总结|概括|归纳|整理)[一下]?(?:核心|主要|关键)?(?:思路|要点|内容|设计)/u,
|
|
488
|
+
/(?:随时|有问题|有需要|需要调整|需要修改|需要补充)\s*(?:联系|找我|告诉|沟通|调整|修改|补充|说)/u,
|
|
489
|
+
/^(?:.{0,40})(?:好的[,,,!\s]|嗯[,,,!\s]|收到[了]?[,,,!\s]|了解[了]?[,,,!\s]|明白[了]?[,,,!\s])/mu,
|
|
490
|
+
/(?:我来|让我|容我)\s*(?:整理|梳理|归纳|想想|看看|重新|检查|核实)/u,
|
|
491
|
+
// 过渡语(需要 2+ hits):"以下是:" / "如下:" 作为独立行。文档中可作小节标题,
|
|
492
|
+
// 但配合另一个对话模式(如"好的")时是典型的 AI 内容包装。
|
|
493
|
+
/^.{0,60}(?:以下是|下面是|如下)\s*[::,,]\s*$/mu
|
|
494
|
+
];
|
|
495
|
+
var CHINESE_TAIL_CLOSING_MARKERS = [
|
|
496
|
+
/(?:需要你|请你|麻烦你|你看)\s*(?:确认|决定|选择|帮忙|看[一看下]|过目|觉得|认为)/u,
|
|
497
|
+
/(?:随时|有问题|有需要|需要调整|需要修改)\s*(?:联系|找我|告诉|沟通|调整|修改)/u,
|
|
498
|
+
/(?:如果|假如|假设)\s*(?:有[没缺][有]?|需要|觉得|认为).{0,30}?(?:随时|可以|直接|尽管)\s*(?:联系|找我|告诉|说|调整|修改)/u,
|
|
499
|
+
/(?:以上|先这样|暂时|目前).{0,10}$/mu,
|
|
500
|
+
/(?:几个|一些|以下)\s*(?:关键)?(?:问题|疑问|想法|建议).{0,30}$/mu
|
|
481
501
|
];
|
|
482
502
|
function detectMetaNarration(content) {
|
|
483
503
|
if (!content) return null;
|
|
@@ -485,7 +505,19 @@ function detectMetaNarration(content) {
|
|
|
485
505
|
for (const re of META_NARRATION_HARD_MARKERS) {
|
|
486
506
|
if (re.test(head)) return re.source;
|
|
487
507
|
}
|
|
488
|
-
if (
|
|
508
|
+
if (content.length > 500) {
|
|
509
|
+
const tail = content.slice(-500);
|
|
510
|
+
let tailHits = 0;
|
|
511
|
+
let tailFirst = "";
|
|
512
|
+
for (const re of CHINESE_TAIL_CLOSING_MARKERS) {
|
|
513
|
+
if (re.test(tail)) {
|
|
514
|
+
tailHits++;
|
|
515
|
+
if (!tailFirst) tailFirst = re.source;
|
|
516
|
+
if (tailHits >= 2) return `meta-narration-tail:${tailFirst}`;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (/^#{1,3}\s+\S/m.test(content.slice(0, 200))) return null;
|
|
489
521
|
let hits = 0;
|
|
490
522
|
let firstMatch = "";
|
|
491
523
|
for (const re of META_NARRATION_HEURISTICS) {
|