@team-semicolon/semo-cli 4.4.3 → 4.6.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/dist/commands/bots.js +118 -0
- package/dist/kb.js +20 -5
- package/package.json +1 -1
package/dist/commands/bots.js
CHANGED
|
@@ -162,6 +162,101 @@ async function detectGatewayStatus(botId) {
|
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
// ============================================================
|
|
165
|
+
// Workspace files sync → bot_workspace_files
|
|
166
|
+
// ============================================================
|
|
167
|
+
const crypto = __importStar(require("crypto"));
|
|
168
|
+
const BINARY_EXTS = new Set([
|
|
169
|
+
'.png', '.jpg', '.jpeg', '.gif', '.ico', '.webp', '.bmp', '.svg',
|
|
170
|
+
'.woff', '.woff2', '.ttf', '.eot', '.otf',
|
|
171
|
+
'.zip', '.tar', '.gz', '.bz2', '.7z', '.rar',
|
|
172
|
+
'.pdf', '.doc', '.docx', '.xls', '.xlsx',
|
|
173
|
+
'.exe', '.dll', '.so', '.dylib', '.bin',
|
|
174
|
+
'.mp3', '.mp4', '.wav', '.avi', '.mov',
|
|
175
|
+
'.db', '.sqlite', '.sqlite3',
|
|
176
|
+
]);
|
|
177
|
+
const MAX_FILE_SIZE = 512 * 1024; // 512KB
|
|
178
|
+
async function syncWorkspaceFiles(client, botId, workspaceDir) {
|
|
179
|
+
const files = [];
|
|
180
|
+
function scan(dir, relBase, depth) {
|
|
181
|
+
if (depth > 4)
|
|
182
|
+
return;
|
|
183
|
+
let entries;
|
|
184
|
+
try {
|
|
185
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
for (const entry of entries) {
|
|
191
|
+
if (entry.name.startsWith('.'))
|
|
192
|
+
continue;
|
|
193
|
+
const fullPath = path.join(dir, entry.name);
|
|
194
|
+
const relPath = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
195
|
+
// Skip symlinks
|
|
196
|
+
try {
|
|
197
|
+
if (fs.lstatSync(fullPath).isSymbolicLink())
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (entry.isDirectory()) {
|
|
204
|
+
scan(fullPath, relPath, depth + 1);
|
|
205
|
+
}
|
|
206
|
+
else if (entry.isFile()) {
|
|
207
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
208
|
+
if (BINARY_EXTS.has(ext))
|
|
209
|
+
continue;
|
|
210
|
+
let stat;
|
|
211
|
+
try {
|
|
212
|
+
stat = fs.statSync(fullPath);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (stat.size > MAX_FILE_SIZE)
|
|
218
|
+
continue;
|
|
219
|
+
let content;
|
|
220
|
+
try {
|
|
221
|
+
content = fs.readFileSync(fullPath, 'utf-8');
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
// Skip files with NULL bytes (binary masquerading as text)
|
|
227
|
+
if (content.includes('\0'))
|
|
228
|
+
continue;
|
|
229
|
+
const hash = crypto.createHash('sha256').update(content).digest('hex');
|
|
230
|
+
files.push({ relPath, content, hash, size: stat.size });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
scan(workspaceDir, '', 0);
|
|
235
|
+
let upserted = 0;
|
|
236
|
+
for (const f of files) {
|
|
237
|
+
const result = await client.query(`INSERT INTO semo.bot_workspace_files (bot_id, file_path, content, file_size, file_hash, synced_at)
|
|
238
|
+
VALUES ($1, $2, $3, $4, $5, NOW())
|
|
239
|
+
ON CONFLICT (bot_id, file_path) DO UPDATE SET
|
|
240
|
+
content = EXCLUDED.content,
|
|
241
|
+
file_size = EXCLUDED.file_size,
|
|
242
|
+
file_hash = EXCLUDED.file_hash,
|
|
243
|
+
synced_at = NOW()
|
|
244
|
+
WHERE semo.bot_workspace_files.file_hash IS DISTINCT FROM EXCLUDED.file_hash`, [botId, f.relPath, f.content, f.size, f.hash]);
|
|
245
|
+
if (result.rowCount && result.rowCount > 0) {
|
|
246
|
+
upserted++;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// Delete files in DB but not on disk (for this bot_id)
|
|
250
|
+
const dbFiles = await client.query(`SELECT file_path FROM semo.bot_workspace_files WHERE bot_id = $1`, [botId]);
|
|
251
|
+
const diskPaths = new Set(files.map(f => f.relPath));
|
|
252
|
+
for (const row of dbFiles.rows) {
|
|
253
|
+
if (!diskPaths.has(row.file_path)) {
|
|
254
|
+
await client.query(`DELETE FROM semo.bot_workspace_files WHERE bot_id = $1 AND file_path = $2`, [botId, row.file_path]);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return files.length;
|
|
258
|
+
}
|
|
259
|
+
// ============================================================
|
|
165
260
|
// Command registration
|
|
166
261
|
// ============================================================
|
|
167
262
|
function registerBotsCommands(program) {
|
|
@@ -454,6 +549,29 @@ function registerBotsCommands(program) {
|
|
|
454
549
|
catch {
|
|
455
550
|
console.log(chalk_1.default.yellow(" ⚠ skills sync 실패 (무시)"));
|
|
456
551
|
}
|
|
552
|
+
// Files piggyback — 워크스페이스 파일 → bot_workspace_files 동기화
|
|
553
|
+
try {
|
|
554
|
+
console.log(chalk_1.default.gray(" → files sync 실행 중..."));
|
|
555
|
+
const filesClient = await pool.connect();
|
|
556
|
+
try {
|
|
557
|
+
let totalFiles = 0;
|
|
558
|
+
for (const bot of bots) {
|
|
559
|
+
totalFiles += await syncWorkspaceFiles(filesClient, bot.botId, bot.workspacePath);
|
|
560
|
+
}
|
|
561
|
+
// Shared files
|
|
562
|
+
const sharedDir = path.join(os.homedir(), '.openclaw-shared');
|
|
563
|
+
if (fs.existsSync(sharedDir)) {
|
|
564
|
+
totalFiles += await syncWorkspaceFiles(filesClient, '_shared', sharedDir);
|
|
565
|
+
}
|
|
566
|
+
console.log(chalk_1.default.green(` → files sync 완료: ${totalFiles}개 파일`));
|
|
567
|
+
}
|
|
568
|
+
finally {
|
|
569
|
+
filesClient.release();
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
catch (filesErr) {
|
|
573
|
+
console.log(chalk_1.default.yellow(` ⚠ files sync 실패 (무시): ${filesErr}`));
|
|
574
|
+
}
|
|
457
575
|
}
|
|
458
576
|
catch (err) {
|
|
459
577
|
await client.query("ROLLBACK");
|
package/dist/kb.js
CHANGED
|
@@ -395,8 +395,22 @@ async function kbSearch(pool, query, options) {
|
|
|
395
395
|
}
|
|
396
396
|
// Text search (fallback or hybrid supplement)
|
|
397
397
|
// Split query into tokens and match ANY token via ILIKE (Korean-friendly)
|
|
398
|
+
// For Korean tokens of 4+ chars with no spaces, add 2-char sub-tokens
|
|
399
|
+
// e.g. "노조관리" → ["노조관리", "노조", "관리"]
|
|
398
400
|
if (mode !== "semantic" || results.length === 0) {
|
|
399
|
-
const
|
|
401
|
+
const rawTokens = query.split(/\s+/).filter(t => t.length >= 2);
|
|
402
|
+
const tokens = [];
|
|
403
|
+
const KOREAN_RE = /[\uAC00-\uD7AF]/;
|
|
404
|
+
for (const t of rawTokens) {
|
|
405
|
+
tokens.push(t);
|
|
406
|
+
if (KOREAN_RE.test(t) && t.length >= 4) {
|
|
407
|
+
for (let i = 0; i + 2 <= t.length; i += 2) {
|
|
408
|
+
const sub = t.slice(i, i + 2);
|
|
409
|
+
if (!tokens.includes(sub))
|
|
410
|
+
tokens.push(sub);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
400
414
|
const textParams = [];
|
|
401
415
|
let tIdx = 1;
|
|
402
416
|
// Build per-token ILIKE conditions + count matching tokens for scoring
|
|
@@ -700,7 +714,7 @@ async function kbUpsert(pool, entry) {
|
|
|
700
714
|
client.release();
|
|
701
715
|
}
|
|
702
716
|
// Key validation against type schema
|
|
703
|
-
|
|
717
|
+
{
|
|
704
718
|
const schemaClient = await pool.connect();
|
|
705
719
|
try {
|
|
706
720
|
const typeResult = await schemaClient.query("SELECT entity_type FROM semo.ontology WHERE domain = $1 AND entity_type IS NOT NULL", [entry.domain]);
|
|
@@ -726,13 +740,14 @@ async function kbUpsert(pool, entry) {
|
|
|
726
740
|
}
|
|
727
741
|
}
|
|
728
742
|
}
|
|
743
|
+
catch (e) {
|
|
744
|
+
// DB 연결 실패 시 warning 로그 — 검증 자체는 스킵하되 사용자에게 알림
|
|
745
|
+
console.error(`[kb] ⚠️ 스키마 검증 DB 오류 (검증 건너뜀): ${e}`);
|
|
746
|
+
}
|
|
729
747
|
finally {
|
|
730
748
|
schemaClient.release();
|
|
731
749
|
}
|
|
732
750
|
}
|
|
733
|
-
catch {
|
|
734
|
-
// Validation failure is non-fatal
|
|
735
|
-
}
|
|
736
751
|
// Generate embedding (mandatory)
|
|
737
752
|
const fullKey = combineKey(key, subKey);
|
|
738
753
|
const text = `${fullKey}: ${entry.content}`;
|