@yeaft/webchat-agent 0.1.471 → 0.1.473

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/routing.js CHANGED
@@ -247,7 +247,7 @@ export async function executeRoute(session, fromRole, route, turnImages = []) {
247
247
  }
248
248
  updateKanban(session, {
249
249
  taskId, taskTitle, assignee: resolvedKanbanTo || to,
250
- status, summary: summary.substring(0, 100)
250
+ status, summary
251
251
  }).catch(e => console.warn(`[Crew] Failed to update kanban:`, e.message));
252
252
  }
253
253
 
@@ -254,6 +254,61 @@ export async function saveRoleWorkSummary(session, roleName, accumulatedText) {
254
254
  // 看板写入锁:防止并发写入
255
255
  let _kanbanWriteLock = Promise.resolve();
256
256
 
257
+ /**
258
+ * 校验 taskId 是否合法。
259
+ * 拒绝:占位符(<task-id>、task-XX)、纯数字、空/带尖括号。
260
+ * 允许:以字母开头、字母数字/连字符/下划线,例如 task-289、fix-crew-xxx。
261
+ *
262
+ * @param {string} id
263
+ * @returns {boolean}
264
+ */
265
+ export function isValidTaskId(id) {
266
+ if (typeof id !== 'string') return false;
267
+ const s = id.trim();
268
+ if (!s) return false;
269
+ if (s.includes('<') || s.includes('>')) return false;
270
+ // explicit placeholder
271
+ if (/^task-x+$/i.test(s)) return false;
272
+ // pure digits (e.g. bare "279")
273
+ if (/^\d+$/.test(s)) return false;
274
+ // must start with a letter and contain only alnum/_/-
275
+ if (!/^[a-zA-Z][a-zA-Z0-9_-]{1,79}$/.test(s)) return false;
276
+ return true;
277
+ }
278
+
279
+ /**
280
+ * 规范化 "最新进展" 摘要:
281
+ * - 换行/回车合并为单空格
282
+ * - 去掉 markdown 分隔符 `---`、heading `##`
283
+ * - 转义 `|` 为 `\|`(避免撑坏表格)
284
+ * - 合并多余空白
285
+ * - 截断到 maxLen 字符(默认 80),超出追加 …
286
+ *
287
+ * @param {string} summary
288
+ * @param {number} [maxLen=80]
289
+ * @returns {string}
290
+ */
291
+ export function sanitizeKanbanSummary(summary, maxLen = 80) {
292
+ if (!summary || typeof summary !== 'string') return '-';
293
+ let s = summary;
294
+ // strip horizontal rule lines and heading markers
295
+ s = s.replace(/^\s*-{3,}\s*$/gm, ' ');
296
+ s = s.replace(/^\s*#{1,6}\s+/gm, '');
297
+ // strip leading list/quote markers on each line
298
+ s = s.replace(/^\s*[-*>]\s+/gm, '');
299
+ // newlines → space
300
+ s = s.replace(/[\r\n]+/g, ' ');
301
+ // escape pipe chars so table doesn't break
302
+ s = s.replace(/\|/g, '\\|');
303
+ // collapse whitespace
304
+ s = s.replace(/\s+/g, ' ').trim();
305
+ if (!s) return '-';
306
+ if (s.length > maxLen) {
307
+ s = s.substring(0, Math.max(1, maxLen - 1)) + '…';
308
+ }
309
+ return s;
310
+ }
311
+
257
312
  /**
258
313
  * 更新工作看板 .crew/context/kanban.md
259
314
  *
@@ -286,12 +341,14 @@ export async function updateKanban(session, opts = {}) {
286
341
  else if (line.startsWith('|') && !line.startsWith('|--') && section) {
287
342
  const cols = line.split('|').map(c => c.trim()).filter(Boolean);
288
343
  if (cols.length >= 3 && cols[0] !== m.colTaskId && cols[0] !== 'task-id') {
344
+ // Skip illegal / placeholder rows (e.g. <task-id>, task-XX, bare "279")
345
+ if (!isValidTaskId(cols[0])) continue;
289
346
  const entry = {
290
347
  taskId: cols[0],
291
- taskTitle: cols[1],
348
+ taskTitle: cols[1] || cols[0],
292
349
  assignee: cols[2] || '-',
293
350
  status: cols[3] || '-',
294
- summary: cols[4] || '-'
351
+ summary: sanitizeKanbanSummary(cols[4] || '-')
295
352
  };
296
353
  if (section === 'completed') {
297
354
  completedEntries.set(entry.taskId, entry);
@@ -306,6 +363,7 @@ export async function updateKanban(session, opts = {}) {
306
363
  // 从 session.features 补充缺失的任务
307
364
  const completed = session._completedTaskIds || new Set();
308
365
  for (const [taskId, feature] of session.features) {
366
+ if (!isValidTaskId(taskId)) continue;
309
367
  if (completed.has(taskId)) {
310
368
  if (!completedEntries.has(taskId)) {
311
369
  completedEntries.set(taskId, {
@@ -330,11 +388,14 @@ export async function updateKanban(session, opts = {}) {
330
388
 
331
389
  // 应用更新
332
390
  if (opts.taskId) {
333
- if (opts.completed) {
391
+ // Reject illegal task ids early (placeholders, bare numbers, angle brackets, etc.)
392
+ if (!isValidTaskId(opts.taskId)) {
393
+ console.warn(`[Crew] updateKanban: rejected invalid taskId "${opts.taskId}"`);
394
+ } else if (opts.completed) {
334
395
  const entry = entries.get(opts.taskId) || completedEntries.get(opts.taskId);
335
396
  if (entry) {
336
397
  entry.status = '✅';
337
- if (opts.summary) entry.summary = opts.summary;
398
+ if (opts.summary) entry.summary = sanitizeKanbanSummary(opts.summary);
338
399
  completedEntries.set(opts.taskId, entry);
339
400
  entries.delete(opts.taskId);
340
401
  }
@@ -353,15 +414,21 @@ export async function updateKanban(session, opts = {}) {
353
414
  if (opts.assignee) entry.assignee = opts.assignee;
354
415
  if (opts.status) entry.status = opts.status;
355
416
  if (opts.summary) {
356
- // 截取摘要
357
- entry.summary = opts.summary.length > 100
358
- ? opts.summary.substring(0, 97) + '...'
359
- : opts.summary;
417
+ // 单行化、去 markdown、转义 |、截断到 80
418
+ entry.summary = sanitizeKanbanSummary(opts.summary);
360
419
  }
361
420
  entries.set(opts.taskId, entry);
362
421
  }
363
422
  }
364
423
 
424
+ // Final safety: drop any lingering invalid ids before writing
425
+ for (const id of Array.from(entries.keys())) {
426
+ if (!isValidTaskId(id)) entries.delete(id);
427
+ }
428
+ for (const id of Array.from(completedEntries.keys())) {
429
+ if (!isValidTaskId(id)) completedEntries.delete(id);
430
+ }
431
+
365
432
  // 生成看板文件
366
433
  const locale = (session.language === 'en') ? 'en-US' : 'zh-CN';
367
434
  const now = new Date().toLocaleString(locale, { timeZone: 'Asia/Shanghai' });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.471",
3
+ "version": "0.1.473",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",