@yeaft/webchat-agent 0.1.473 → 0.1.474
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/crew/session.js +13 -0
- package/crew/task-files.js +58 -4
- package/package.json +1 -1
package/crew/session.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from './persistence.js';
|
|
14
14
|
import { sendCrewMessage, sendCrewOutput, sendStatusUpdate } from './ui-messages.js';
|
|
15
15
|
import { preloadSlashCommands } from '../conversation.js';
|
|
16
|
+
import { sanitizeKanbanFile } from './task-files.js';
|
|
16
17
|
|
|
17
18
|
// =====================================================================
|
|
18
19
|
// Data Structures
|
|
@@ -221,6 +222,13 @@ export async function createCrewSession(msg) {
|
|
|
221
222
|
|
|
222
223
|
crewSessions.set(sessionId, session);
|
|
223
224
|
|
|
225
|
+
// Startup self-heal: rewrite kanban.md once to drop any historical dirty
|
|
226
|
+
// rows (placeholders, bare ids, multi-line summaries) even if no further
|
|
227
|
+
// task-update happens this session.
|
|
228
|
+
sanitizeKanbanFile(session).catch(e =>
|
|
229
|
+
console.warn('[Crew] Kanban startup migration failed:', e.message)
|
|
230
|
+
);
|
|
231
|
+
|
|
224
232
|
// 如果有旧消息,检查是否有更早的分片
|
|
225
233
|
const hasOlderMessages = oldMeta ? await getMaxShardIndex(sharedDir) > 0 : false;
|
|
226
234
|
|
|
@@ -519,6 +527,11 @@ export async function resumeCrewSession(msg) {
|
|
|
519
527
|
};
|
|
520
528
|
crewSessions.set(sessionId, session);
|
|
521
529
|
|
|
530
|
+
// Startup self-heal on resume as well
|
|
531
|
+
sanitizeKanbanFile(session).catch(e =>
|
|
532
|
+
console.warn('[Crew] Kanban startup migration failed:', e.message)
|
|
533
|
+
);
|
|
534
|
+
|
|
522
535
|
const loaded = await loadSessionMessages(session.sharedDir);
|
|
523
536
|
session.uiMessages = loaded.messages;
|
|
524
537
|
|
package/crew/task-files.js
CHANGED
|
@@ -288,16 +288,20 @@ export function isValidTaskId(id) {
|
|
|
288
288
|
* @param {number} [maxLen=80]
|
|
289
289
|
* @returns {string}
|
|
290
290
|
*/
|
|
291
|
-
export function sanitizeKanbanSummary(summary, maxLen =
|
|
291
|
+
export function sanitizeKanbanSummary(summary, maxLen = 120) {
|
|
292
292
|
if (!summary || typeof summary !== 'string') return '-';
|
|
293
293
|
let s = summary;
|
|
294
|
-
// strip horizontal rule lines and heading markers
|
|
294
|
+
// strip horizontal rule lines and heading markers (start-of-line form)
|
|
295
295
|
s = s.replace(/^\s*-{3,}\s*$/gm, ' ');
|
|
296
296
|
s = s.replace(/^\s*#{1,6}\s+/gm, '');
|
|
297
297
|
// strip leading list/quote markers on each line
|
|
298
298
|
s = s.replace(/^\s*[-*>]\s+/gm, '');
|
|
299
299
|
// newlines → space
|
|
300
300
|
s = s.replace(/[\r\n]+/g, ' ');
|
|
301
|
+
// Also strip markdown structural artifacts that may appear mid-string
|
|
302
|
+
// after newline-folding (e.g. "priority: high --- ## obs").
|
|
303
|
+
s = s.replace(/(^|\s)-{3,}(\s|$)/g, ' ');
|
|
304
|
+
s = s.replace(/(^|\s)#{1,6}(\s|$)/g, ' ');
|
|
301
305
|
// escape pipe chars so table doesn't break
|
|
302
306
|
s = s.replace(/\|/g, '\\|');
|
|
303
307
|
// collapse whitespace
|
|
@@ -309,6 +313,24 @@ export function sanitizeKanbanSummary(summary, maxLen = 80) {
|
|
|
309
313
|
return s;
|
|
310
314
|
}
|
|
311
315
|
|
|
316
|
+
/**
|
|
317
|
+
* 规范化非 summary 单元格(taskId / title / assignee / status):
|
|
318
|
+
* 折行、转义 `|`、去首尾空白、限制到 120 字符。
|
|
319
|
+
*
|
|
320
|
+
* @param {string} v
|
|
321
|
+
* @returns {string}
|
|
322
|
+
*/
|
|
323
|
+
export function sanitizeKanbanCell(v) {
|
|
324
|
+
if (v === null || v === undefined) return '-';
|
|
325
|
+
let s = String(v);
|
|
326
|
+
s = s.replace(/[\r\n]+/g, ' ');
|
|
327
|
+
s = s.replace(/\|/g, '\\|');
|
|
328
|
+
s = s.replace(/\s+/g, ' ').trim();
|
|
329
|
+
if (!s) return '-';
|
|
330
|
+
if (s.length > 120) s = s.substring(0, 119) + '…';
|
|
331
|
+
return s;
|
|
332
|
+
}
|
|
333
|
+
|
|
312
334
|
/**
|
|
313
335
|
* 更新工作看板 .crew/context/kanban.md
|
|
314
336
|
*
|
|
@@ -434,12 +456,21 @@ export async function updateKanban(session, opts = {}) {
|
|
|
434
456
|
const now = new Date().toLocaleString(locale, { timeZone: 'Asia/Shanghai' });
|
|
435
457
|
let content = `${m.kanbanTitle}\n> ${m.lastUpdated}: ${now}\n`;
|
|
436
458
|
|
|
459
|
+
// Serialize-layer defense: sanitize every cell one last time so nothing
|
|
460
|
+
// bypasses the table via an unsanitized upstream write path.
|
|
461
|
+
const serializeCell = (v) => sanitizeKanbanCell(v);
|
|
462
|
+
const serializeSummary = (v) => {
|
|
463
|
+
const s = sanitizeKanbanSummary(v);
|
|
464
|
+
// sanitizeKanbanSummary returns "-" for empty, keep that
|
|
465
|
+
return s;
|
|
466
|
+
};
|
|
467
|
+
|
|
437
468
|
const activeArr = Array.from(entries.values());
|
|
438
469
|
content += `\n## 🔨 ${m.kanbanActive} (${activeArr.length})\n`;
|
|
439
470
|
if (activeArr.length > 0) {
|
|
440
471
|
content += `| ${m.colTaskId} | ${m.colTitle} | ${m.kanbanColAssignee} | ${m.kanbanColStatus} | ${m.kanbanColSummary} |\n|---------|------|--------|------|----------|\n`;
|
|
441
472
|
for (const e of activeArr) {
|
|
442
|
-
content += `| ${e.taskId} | ${e.taskTitle} | ${e.assignee} | ${e.status} | ${e.summary} |\n`;
|
|
473
|
+
content += `| ${serializeCell(e.taskId)} | ${serializeCell(e.taskTitle)} | ${serializeCell(e.assignee)} | ${serializeCell(e.status)} | ${serializeSummary(e.summary)} |\n`;
|
|
443
474
|
}
|
|
444
475
|
}
|
|
445
476
|
|
|
@@ -448,7 +479,7 @@ export async function updateKanban(session, opts = {}) {
|
|
|
448
479
|
if (doneArr.length > 0) {
|
|
449
480
|
content += `| ${m.colTaskId} | ${m.colTitle} | ${m.kanbanColAssignee} |\n|---------|------|--------|\n`;
|
|
450
481
|
for (const e of doneArr) {
|
|
451
|
-
content += `| ${e.taskId} | ${e.taskTitle} | ${e.assignee} |\n`;
|
|
482
|
+
content += `| ${serializeCell(e.taskId)} | ${serializeCell(e.taskTitle)} | ${serializeCell(e.assignee)} |\n`;
|
|
452
483
|
}
|
|
453
484
|
}
|
|
454
485
|
|
|
@@ -461,6 +492,29 @@ export async function updateKanban(session, opts = {}) {
|
|
|
461
492
|
return _kanbanWriteLock;
|
|
462
493
|
}
|
|
463
494
|
|
|
495
|
+
/**
|
|
496
|
+
* 启动自愈迁移:立即重写 kanban.md 一次。
|
|
497
|
+
* 触发完整 parse → filter → sanitize → serialize 流程,
|
|
498
|
+
* 不依赖下次 task-update 就能清除历史脏数据。
|
|
499
|
+
*
|
|
500
|
+
* 幂等:文件不存在时不创建。
|
|
501
|
+
*
|
|
502
|
+
* @param {object} session
|
|
503
|
+
* @returns {Promise<boolean>} 是否执行了清理
|
|
504
|
+
*/
|
|
505
|
+
export async function sanitizeKanbanFile(session) {
|
|
506
|
+
if (!session?.sharedDir) return false;
|
|
507
|
+
const kanbanPath = join(session.sharedDir, 'context', 'kanban.md');
|
|
508
|
+
try {
|
|
509
|
+
await fs.access(kanbanPath);
|
|
510
|
+
} catch {
|
|
511
|
+
return false; // 文件不存在,无需清理
|
|
512
|
+
}
|
|
513
|
+
await updateKanban(session, {}); // 无 opts → 纯重写
|
|
514
|
+
console.log(`[Crew] Kanban startup migration completed: ${kanbanPath}`);
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
|
|
464
518
|
/**
|
|
465
519
|
* 读取看板文件内容
|
|
466
520
|
*/
|