shabti 2.12.0 → 2.12.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 +1 -1
- package/src/a2a/server.js +63 -11
- package/src/commands/config.js +50 -2
- package/src/commands/export.js +4 -2
- package/src/commands/import.js +5 -3
- package/src/commands/snapshot.js +6 -0
- package/src/commands/status.js +0 -2
- package/src/core/engine.js +15 -6
- package/src/core/session.js +18 -0
- package/src/mcp/server.js +13 -4
- package/src/repl/slashCommands.js +8 -3
- package/src/utils/validate.js +17 -0
package/package.json
CHANGED
package/src/a2a/server.js
CHANGED
|
@@ -10,8 +10,13 @@ import { normalizeText } from "../utils/normalize.js";
|
|
|
10
10
|
// ============================================================
|
|
11
11
|
|
|
12
12
|
export class TaskStore {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
#tasks = new Map();
|
|
14
|
+
#maxTasks;
|
|
15
|
+
#ttlMs;
|
|
16
|
+
|
|
17
|
+
constructor({ maxTasks = 1000, ttlMs = 30 * 60 * 1000 } = {}) {
|
|
18
|
+
this.#maxTasks = maxTasks;
|
|
19
|
+
this.#ttlMs = ttlMs;
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
create(contextId) {
|
|
@@ -22,21 +27,44 @@ export class TaskStore {
|
|
|
22
27
|
status: { state: "submitted", timestamp: new Date().toISOString() },
|
|
23
28
|
artifacts: [],
|
|
24
29
|
history: [],
|
|
30
|
+
createdAt: Date.now(),
|
|
25
31
|
};
|
|
26
|
-
this
|
|
32
|
+
this.#tasks.set(task.id, task);
|
|
33
|
+
|
|
34
|
+
if (this.#tasks.size > this.#maxTasks) {
|
|
35
|
+
this.#evictOldest();
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
return task;
|
|
28
39
|
}
|
|
29
40
|
|
|
41
|
+
#evictOldest() {
|
|
42
|
+
const oldest = [...this.#tasks.entries()].sort(([, a], [, b]) => a.createdAt - b.createdAt)[0];
|
|
43
|
+
if (oldest) this.#tasks.delete(oldest[0]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
cleanup() {
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
for (const [id, task] of this.#tasks) {
|
|
49
|
+
if (
|
|
50
|
+
now - task.createdAt > this.#ttlMs &&
|
|
51
|
+
["completed", "canceled", "failed"].includes(task.status.state)
|
|
52
|
+
) {
|
|
53
|
+
this.#tasks.delete(id);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
30
58
|
get(id) {
|
|
31
|
-
return this
|
|
59
|
+
return this.#tasks.get(id) || null;
|
|
32
60
|
}
|
|
33
61
|
|
|
34
62
|
set(id, task) {
|
|
35
|
-
this
|
|
63
|
+
this.#tasks.set(id, task);
|
|
36
64
|
}
|
|
37
65
|
|
|
38
66
|
clear() {
|
|
39
|
-
this
|
|
67
|
+
this.#tasks.clear();
|
|
40
68
|
}
|
|
41
69
|
}
|
|
42
70
|
|
|
@@ -403,10 +431,19 @@ function handleTasksCancel(taskStore, params) {
|
|
|
403
431
|
// HTTP Server
|
|
404
432
|
// ============================================================
|
|
405
433
|
|
|
406
|
-
function readBody(req) {
|
|
434
|
+
function readBody(req, maxBytes = 1_048_576) {
|
|
435
|
+
// デフォルト1MB上限
|
|
407
436
|
return new Promise((resolve, reject) => {
|
|
408
437
|
const chunks = [];
|
|
409
|
-
|
|
438
|
+
let total = 0;
|
|
439
|
+
req.on("data", (c) => {
|
|
440
|
+
total += c.length;
|
|
441
|
+
if (total > maxBytes) {
|
|
442
|
+
req.destroy();
|
|
443
|
+
return reject(new Error("リクエストボディが大きすぎます"));
|
|
444
|
+
}
|
|
445
|
+
chunks.push(c);
|
|
446
|
+
});
|
|
410
447
|
req.on("end", () => resolve(Buffer.concat(chunks).toString()));
|
|
411
448
|
req.on("error", reject);
|
|
412
449
|
});
|
|
@@ -434,17 +471,30 @@ export function startA2AServer(port = 3000) {
|
|
|
434
471
|
const agentCard = buildAgentCard(baseUrl);
|
|
435
472
|
const taskStore = new TaskStore();
|
|
436
473
|
|
|
474
|
+
const cleanupInterval = setInterval(() => taskStore.cleanup(), 5 * 60 * 1000);
|
|
475
|
+
|
|
437
476
|
const server = createServer(async (req, res) => {
|
|
438
|
-
// CORS headers
|
|
439
|
-
res.setHeader("Access-Control-Allow-Origin", "
|
|
477
|
+
// CORS headers — restrict to null (no browser cross-origin access needed for localhost)
|
|
478
|
+
res.setHeader("Access-Control-Allow-Origin", "null");
|
|
440
479
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
441
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
480
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
442
481
|
|
|
443
482
|
if (req.method === "OPTIONS") {
|
|
444
483
|
res.writeHead(204);
|
|
445
484
|
return res.end();
|
|
446
485
|
}
|
|
447
486
|
|
|
487
|
+
// Bearer Token authentication
|
|
488
|
+
const a2aToken = process.env.SHABTI_A2A_TOKEN;
|
|
489
|
+
if (a2aToken) {
|
|
490
|
+
const authHeader = req.headers["authorization"] || "";
|
|
491
|
+
if (!authHeader.startsWith("Bearer ") || authHeader.slice(7) !== a2aToken) {
|
|
492
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
493
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
448
498
|
// Agent Card discovery
|
|
449
499
|
if (req.method === "GET" && req.url === "/.well-known/agent-card.json") {
|
|
450
500
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -495,6 +545,8 @@ export function startA2AServer(port = 3000) {
|
|
|
495
545
|
res.end("Not Found");
|
|
496
546
|
});
|
|
497
547
|
|
|
548
|
+
server.on("close", () => clearInterval(cleanupInterval));
|
|
549
|
+
|
|
498
550
|
server.listen(port, "127.0.0.1", () => {
|
|
499
551
|
console.log(`\n Shabti A2A server listening on ${baseUrl}`);
|
|
500
552
|
console.log(` Agent Card: ${baseUrl}.well-known/agent-card.json`);
|
package/src/commands/config.js
CHANGED
|
@@ -1,9 +1,38 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { loadConfig, saveConfig, DEFAULT_CONFIG } from "../core/engine.js";
|
|
3
3
|
import { success, error, heading } from "../utils/style.js";
|
|
4
|
+
import { validateQdrantUrl } from "../utils/validate.js";
|
|
4
5
|
|
|
5
6
|
const ALLOWED_KEYS = Object.keys(DEFAULT_CONFIG);
|
|
6
7
|
|
|
8
|
+
// キーごとのバリデーター
|
|
9
|
+
const CONFIG_VALIDATORS = {
|
|
10
|
+
qdrant_url: (value) => {
|
|
11
|
+
try {
|
|
12
|
+
const url = new URL(value);
|
|
13
|
+
if (!["http:", "https:"].includes(url.protocol)) {
|
|
14
|
+
throw new Error("http://またはhttps://のみ使用できます");
|
|
15
|
+
}
|
|
16
|
+
} catch (e) {
|
|
17
|
+
throw new Error(`qdrant_urlが無効です: ${e.message}`, { cause: e });
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
data_dir: (value) => {
|
|
21
|
+
const { resolve } = require("path");
|
|
22
|
+
const abs = resolve(value);
|
|
23
|
+
// 危険なシステムパスを拒否
|
|
24
|
+
const dangerousPaths = ["/etc", "/usr", "/bin", "/sbin", "/sys", "/proc", "/dev"];
|
|
25
|
+
if (dangerousPaths.some((p) => abs.startsWith(p))) {
|
|
26
|
+
throw new Error(`data_dirにシステムパスは指定できません: ${abs}`);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
collection_name: (value) => {
|
|
30
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(value) || value.length > 256) {
|
|
31
|
+
throw new Error("collection_nameは英数字、ハイフン、アンダースコアのみ使用できます");
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
7
36
|
export function registerConfig(program) {
|
|
8
37
|
const cmd = program.command("config").description("Manage shabti configuration");
|
|
9
38
|
|
|
@@ -39,10 +68,29 @@ export function registerConfig(program) {
|
|
|
39
68
|
return;
|
|
40
69
|
}
|
|
41
70
|
|
|
71
|
+
if (CONFIG_VALIDATORS[key]) {
|
|
72
|
+
try {
|
|
73
|
+
CONFIG_VALIDATORS[key](value);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
error(e.message);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
42
80
|
const config = loadConfig();
|
|
43
|
-
|
|
81
|
+
let validatedValue = value;
|
|
82
|
+
if (key === "qdrant_url") {
|
|
83
|
+
try {
|
|
84
|
+
validatedValue = validateQdrantUrl(value);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
error(err.message);
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
config[key] = validatedValue;
|
|
44
92
|
saveConfig(config);
|
|
45
|
-
success(`${key} = ${
|
|
93
|
+
success(`${key} = ${validatedValue}`);
|
|
46
94
|
});
|
|
47
95
|
|
|
48
96
|
cmd
|
package/src/commands/export.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { writeFileSync } from "fs";
|
|
2
|
+
import { resolve } from "path";
|
|
2
3
|
import { createEngine } from "../core/engine.js";
|
|
3
4
|
import { success, error } from "../utils/style.js";
|
|
4
5
|
import { parsePositiveInt } from "../utils/validate.js";
|
|
@@ -28,8 +29,9 @@ export function registerExport(program) {
|
|
|
28
29
|
const output = lines.join("\n");
|
|
29
30
|
|
|
30
31
|
if (opts.output) {
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
const absOutput = resolve(opts.output);
|
|
33
|
+
writeFileSync(absOutput, output + (lines.length ? "\n" : ""), "utf8");
|
|
34
|
+
success(`Exported ${entries.length} entries to ${absOutput}`);
|
|
33
35
|
} else {
|
|
34
36
|
if (output) console.log(output);
|
|
35
37
|
// Print summary to stderr so it doesn't pollute piped output
|
package/src/commands/import.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from "fs";
|
|
2
|
+
import { resolve } from "path";
|
|
2
3
|
import { createEngine } from "../core/engine.js";
|
|
3
4
|
import { success, error, info, warn } from "../utils/style.js";
|
|
4
5
|
|
|
@@ -11,12 +12,13 @@ export function registerImport(program) {
|
|
|
11
12
|
.option("--dry-run", "Parse and validate without storing")
|
|
12
13
|
.action(async (file, opts) => {
|
|
13
14
|
try {
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
const absFile = resolve(file);
|
|
16
|
+
if (!existsSync(absFile)) {
|
|
17
|
+
error(`File not found: ${absFile}`);
|
|
16
18
|
process.exitCode = 1;
|
|
17
19
|
return;
|
|
18
20
|
}
|
|
19
|
-
const raw = readFileSync(
|
|
21
|
+
const raw = readFileSync(absFile, "utf8");
|
|
20
22
|
const lines = raw
|
|
21
23
|
.split("\n")
|
|
22
24
|
.map((l) => l.trim())
|
package/src/commands/snapshot.js
CHANGED
|
@@ -28,6 +28,12 @@ export function registerSnapshot(program) {
|
|
|
28
28
|
.argument("<id>", "Snapshot ID to restore")
|
|
29
29
|
.action(async (id) => {
|
|
30
30
|
try {
|
|
31
|
+
// UUIDフォーマット検証
|
|
32
|
+
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
33
|
+
if (!UUID_REGEX.test(id)) {
|
|
34
|
+
error(`無効なスナップショットID: ${id}(UUID形式で指定してください)`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
31
37
|
const engine = createEngine();
|
|
32
38
|
engine.snapshotRestore(id);
|
|
33
39
|
success(`Restored from snapshot: ${id}`);
|
package/src/commands/status.js
CHANGED
|
@@ -19,8 +19,6 @@ export function registerStatus(program) {
|
|
|
19
19
|
console.log();
|
|
20
20
|
console.log(` ${chalk.cyan("Entries:")} ${status.entryCount}`);
|
|
21
21
|
console.log(` ${chalk.cyan("Tier:")} ${status.tier}`);
|
|
22
|
-
console.log(` ${chalk.cyan("Qdrant URL:")} ${status.qdrantUrl}`);
|
|
23
|
-
console.log(` ${chalk.cyan("Data Dir:")} ${status.dataDir}`);
|
|
24
22
|
console.log(` ${chalk.cyan("Model:")} ${status.modelId}`);
|
|
25
23
|
console.log();
|
|
26
24
|
}
|
package/src/core/engine.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { readFileSync, existsSync, mkdirSync, writeFileSync } from "fs";
|
|
1
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync, renameSync } from "fs";
|
|
2
2
|
import { createRequire } from "module";
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
import { join } from "path";
|
|
5
|
+
import { validateQdrantUrl } from "../utils/validate.js";
|
|
5
6
|
|
|
6
7
|
const require = createRequire(import.meta.url);
|
|
7
8
|
|
|
@@ -17,7 +18,9 @@ const DEFAULT_CONFIG = {
|
|
|
17
18
|
/** Env var overrides (highest priority). */
|
|
18
19
|
function envOverrides() {
|
|
19
20
|
const o = {};
|
|
20
|
-
if (process.env.SHABTI_QDRANT_URL)
|
|
21
|
+
if (process.env.SHABTI_QDRANT_URL) {
|
|
22
|
+
o.qdrant_url = validateQdrantUrl(process.env.SHABTI_QDRANT_URL);
|
|
23
|
+
}
|
|
21
24
|
return o;
|
|
22
25
|
}
|
|
23
26
|
|
|
@@ -25,7 +28,11 @@ export function loadConfig() {
|
|
|
25
28
|
let config = DEFAULT_CONFIG;
|
|
26
29
|
if (existsSync(CONFIG_PATH)) {
|
|
27
30
|
try {
|
|
28
|
-
|
|
31
|
+
const saved = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
|
32
|
+
if (saved.qdrant_url !== undefined) {
|
|
33
|
+
saved.qdrant_url = validateQdrantUrl(saved.qdrant_url);
|
|
34
|
+
}
|
|
35
|
+
config = { ...config, ...saved };
|
|
29
36
|
} catch (_) {
|
|
30
37
|
// keep defaults
|
|
31
38
|
}
|
|
@@ -34,13 +41,15 @@ export function loadConfig() {
|
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
export function saveConfig(config) {
|
|
37
|
-
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
38
|
-
|
|
44
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
45
|
+
const tmpPath = CONFIG_PATH + ".tmp";
|
|
46
|
+
writeFileSync(tmpPath, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
47
|
+
renameSync(tmpPath, CONFIG_PATH);
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
export function createEngine(configOverrides = {}) {
|
|
42
51
|
const config = { ...loadConfig(), ...configOverrides };
|
|
43
|
-
mkdirSync(config.data_dir, { recursive: true });
|
|
52
|
+
mkdirSync(config.data_dir, { recursive: true, mode: 0o700 });
|
|
44
53
|
|
|
45
54
|
const { ShabtiEngine } = require("../../native.cjs");
|
|
46
55
|
|
package/src/core/session.js
CHANGED
|
@@ -6,6 +6,10 @@ import { error } from "../utils/style.js";
|
|
|
6
6
|
const DEFAULT_SYSTEM_PROMPT =
|
|
7
7
|
"You are Shabti, a helpful assistant accessed via CLI. Be concise and direct.";
|
|
8
8
|
|
|
9
|
+
// 既知の有効なOpenAIモデル名のパターン
|
|
10
|
+
const ALLOWED_MODEL_PATTERN = /^(gpt-[34][- .\w]+|o[12][\w-]*)$/;
|
|
11
|
+
const MAX_HISTORY_TURNS = 50; // 最大50ターン
|
|
12
|
+
|
|
9
13
|
export class ChatSession {
|
|
10
14
|
#client;
|
|
11
15
|
#messages;
|
|
@@ -49,9 +53,22 @@ export class ChatSession {
|
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
setModel(model) {
|
|
56
|
+
if (!ALLOWED_MODEL_PATTERN.test(model)) {
|
|
57
|
+
throw new Error(`無効なモデル名: ${model}`);
|
|
58
|
+
}
|
|
52
59
|
this.#model = model;
|
|
53
60
|
}
|
|
54
61
|
|
|
62
|
+
#trimHistory() {
|
|
63
|
+
// システムメッセージは保持、ユーザー/アシスタントのペアを最新N件に制限
|
|
64
|
+
const systemMessages = this.#messages.filter((m) => m.role === "system");
|
|
65
|
+
const conversationMessages = this.#messages.filter((m) => m.role !== "system");
|
|
66
|
+
|
|
67
|
+
if (conversationMessages.length > MAX_HISTORY_TURNS * 2) {
|
|
68
|
+
this.#messages = [...systemMessages, ...conversationMessages.slice(-MAX_HISTORY_TURNS * 2)];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
55
72
|
getModel() {
|
|
56
73
|
return this.#model;
|
|
57
74
|
}
|
|
@@ -84,6 +101,7 @@ export class ChatSession {
|
|
|
84
101
|
}
|
|
85
102
|
|
|
86
103
|
this.#messages.push({ role: "user", content: trimmed });
|
|
104
|
+
this.#trimHistory();
|
|
87
105
|
|
|
88
106
|
try {
|
|
89
107
|
process.stdout.write(chalk.cyan("shabti: "));
|
package/src/mcp/server.js
CHANGED
|
@@ -226,7 +226,6 @@ async function handleToolsCall(id, params) {
|
|
|
226
226
|
entry_count: status.entryCount,
|
|
227
227
|
tier: status.tier,
|
|
228
228
|
model_id: status.modelId,
|
|
229
|
-
qdrant_url: status.qdrantUrl,
|
|
230
229
|
},
|
|
231
230
|
null,
|
|
232
231
|
2,
|
|
@@ -280,8 +279,10 @@ async function handleToolsCall(id, params) {
|
|
|
280
279
|
const queryObj = { text: normalizeText(query), limit };
|
|
281
280
|
if (args.namespace) queryObj.namespace = args.namespace;
|
|
282
281
|
const results = await eng.executeQuery(queryObj);
|
|
282
|
+
const formatMemoryEntry = (entry) =>
|
|
283
|
+
`[MEMORY_START id="${entry.id}"]\n${entry.content}\n[MEMORY_END]`;
|
|
283
284
|
const formatted = results.map((r) => ({
|
|
284
|
-
content: r
|
|
285
|
+
content: formatMemoryEntry(r),
|
|
285
286
|
score: r.score,
|
|
286
287
|
id: r.id,
|
|
287
288
|
namespace: r.namespace,
|
|
@@ -503,12 +504,17 @@ function handleResourcesRead(id, params) {
|
|
|
503
504
|
|
|
504
505
|
if (uri === "shabti://config") {
|
|
505
506
|
const config = loadConfig();
|
|
507
|
+
// 機密フィールドを除いた安全な設定情報のみ返す
|
|
508
|
+
const safeConfig = {
|
|
509
|
+
collection_name: config.collection_name,
|
|
510
|
+
// qdrant_url, data_dir は公開しない
|
|
511
|
+
};
|
|
506
512
|
return respond(id, {
|
|
507
513
|
contents: [
|
|
508
514
|
{
|
|
509
515
|
uri,
|
|
510
516
|
mimeType: "application/json",
|
|
511
|
-
text: JSON.stringify(
|
|
517
|
+
text: JSON.stringify(safeConfig),
|
|
512
518
|
},
|
|
513
519
|
],
|
|
514
520
|
});
|
|
@@ -549,7 +555,10 @@ async function handleRequest(line) {
|
|
|
549
555
|
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
550
556
|
rl.on("line", (line) => {
|
|
551
557
|
const trimmed = line.trim();
|
|
552
|
-
if (trimmed
|
|
558
|
+
if (trimmed && trimmed.length <= 10_000_000) {
|
|
559
|
+
// 10MB上限
|
|
560
|
+
handleRequest(trimmed);
|
|
561
|
+
}
|
|
553
562
|
});
|
|
554
563
|
|
|
555
564
|
async function shutdown() {
|
|
@@ -48,8 +48,12 @@ export function handleSlashCommand(cmd, args, session, rl, engine = null) {
|
|
|
48
48
|
|
|
49
49
|
case "/model":
|
|
50
50
|
if (args) {
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
try {
|
|
52
|
+
session.setModel(args);
|
|
53
|
+
success(`Model switched to ${chalk.cyan(args)}`);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
error(err.message);
|
|
56
|
+
}
|
|
53
57
|
} else {
|
|
54
58
|
info(`Current model: ${chalk.cyan(session.getModel())}`);
|
|
55
59
|
}
|
|
@@ -132,7 +136,8 @@ async function handleRecall(query, engine) {
|
|
|
132
136
|
console.log(chalk.dim(" No memories found."));
|
|
133
137
|
} else {
|
|
134
138
|
for (const r of results) {
|
|
135
|
-
|
|
139
|
+
const formatted = `[MEMORY_START id="${r.id}"]\n${r.content}\n[MEMORY_END]`;
|
|
140
|
+
console.log(` ${chalk.yellow(r.score.toFixed(4))} ${formatted}`);
|
|
136
141
|
}
|
|
137
142
|
}
|
|
138
143
|
} catch (err) {
|
package/src/utils/validate.js
CHANGED
|
@@ -52,3 +52,20 @@ export function parsePort(value, name) {
|
|
|
52
52
|
}
|
|
53
53
|
return n;
|
|
54
54
|
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* qdrant_urlのSSRF防止バリデーション
|
|
58
|
+
* http/httpsスキームのみ許可し、非ローカルホストの場合は警告を出す
|
|
59
|
+
*/
|
|
60
|
+
export function validateQdrantUrl(raw) {
|
|
61
|
+
let parsed;
|
|
62
|
+
try {
|
|
63
|
+
parsed = new URL(raw);
|
|
64
|
+
} catch {
|
|
65
|
+
throw new Error(`無効なqdrant_url: ${raw}`);
|
|
66
|
+
}
|
|
67
|
+
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
68
|
+
throw new Error(`qdrant_urlにはhttp://またはhttps://のみ使用できます: ${raw}`);
|
|
69
|
+
}
|
|
70
|
+
return raw;
|
|
71
|
+
}
|