@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 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
 
@@ -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 = 80) {
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
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.473",
3
+ "version": "0.1.474",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",