agentquad 0.4.5 → 0.4.8

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.
Files changed (40) hide show
  1. package/README.md +4 -3
  2. package/dist-web/assets/index-DdqC2CwH.css +32 -0
  3. package/dist-web/assets/{index-By--XlP3.js → index-DkI6ZJx_.js} +411 -399
  4. package/dist-web/assets/logo-Cxw7XzHl.png +0 -0
  5. package/dist-web/favicon.png +0 -0
  6. package/dist-web/index.html +2 -2
  7. package/package.json +7 -1
  8. package/src/claude-prompt-detector.js +72 -0
  9. package/src/cli.js +1 -1
  10. package/src/codex-hook-installer.js +1 -1
  11. package/src/codex-prompt-detector.js +104 -13
  12. package/src/config.js +33 -5
  13. package/src/db.js +77 -31
  14. package/src/export/todoMarkdown.js +1 -9
  15. package/src/lark-bot.js +44 -5
  16. package/src/mcp/tools/destructive/index.js +22 -16
  17. package/src/mcp/tools/openclaw/index.js +12 -16
  18. package/src/mcp/tools/read/index.js +7 -7
  19. package/src/mcp/tools/write/index.js +9 -6
  20. package/src/openclaw-bridge.js +176 -28
  21. package/src/openclaw-hook-installer.js +2 -1
  22. package/src/openclaw-hook.js +127 -9
  23. package/src/openclaw-wizard.js +168 -191
  24. package/src/permission-prompt.js +113 -31
  25. package/src/prompt-render.js +0 -8
  26. package/src/pty.js +183 -49
  27. package/src/routes/ai-terminal.js +90 -26
  28. package/src/routes/telegram-sync.js +7 -5
  29. package/src/routes/todos.js +8 -14
  30. package/src/server.js +90 -12
  31. package/src/session-input-dispatcher.js +48 -4
  32. package/src/stats/report.js +1 -6
  33. package/src/telegram-bot.js +82 -15
  34. package/src/telegram-loading-status.js +1 -1
  35. package/src/templates/claude-hooks/notify.js +1 -1
  36. package/src/templates/codex-hooks/notify.js +1 -1
  37. package/src/wiki/index.js +1 -1
  38. package/src/wiki/sources.js +0 -1
  39. package/dist-web/assets/index-8A0oLLcX.css +0 -32
  40. package/dist-web/assets/logo-D4DDtU-r.png +0 -0
@@ -5,7 +5,7 @@
5
5
  * 设计目标:
6
6
  * - 一条 inbound 消息 → 一个完整的判断 → 一个 reply 字符串
7
7
  * - 状态机存内存(重启丢失也无所谓,向导本来就是短生命周期)
8
- * - 一句话直说能跳过任意向导步骤("目录 X" / "象限 N" / "模板 Y")
8
+ * - 一句话直说能跳过任意向导步骤("目录 X" / "Agent Y")
9
9
  * - 与 ask_user pending 共存:wizard 优先,wizard 完成后再吃 ask_user 答复
10
10
  *
11
11
  * 路由优先级(handleInbound 内):
@@ -49,17 +49,9 @@ import { resolveTool } from './dispatch.js'
49
49
  const WIZARD_TIMEOUT_MS = 10 * 60 * 1000
50
50
 
51
51
  const STEP_WORKDIR = 'workdir'
52
- const STEP_QUADRANT = 'quadrant'
53
- const STEP_TEMPLATE = 'template'
52
+ const STEP_TEMPLATE = 'template' // 用户面叫 "Agent";代码内部沿用 template key
54
53
  const STEP_DONE = 'done'
55
54
 
56
- const QUADRANTS = [
57
- { id: 1, label: '重要紧急' },
58
- { id: 2, label: '重要不紧急' },
59
- { id: 3, label: '紧急不重要' },
60
- { id: 4, label: '不重要不紧急' },
61
- ]
62
-
63
55
  function telegramPermissionMode(cfg = {}) {
64
56
  const mode = cfg.telegram?.defaultPermissionMode
65
57
  return ['default', 'acceptEdits', 'bypass'].includes(mode) ? mode : 'bypass'
@@ -99,18 +91,12 @@ function tryExtractWorkdir(text) {
99
91
  return null
100
92
  }
101
93
 
102
- /** 解析"象限 N" / "quadrant N" / "Q1" 等;返回 1-4 或 null */
103
- function tryExtractQuadrant(text) {
104
- const m1 = text.match(/(?:象限|quadrant|q)[::=\s]*([1-4])\b/i)
105
- if (m1) return Number(m1[1])
106
- return null
107
- }
108
-
109
- /** 解析"模板 Bug" / "用 X 模板" 等;返回模板名 (string) 或 null */
94
+ /** 解析"agent Bug" / " X agent" / "员工 X" 等;返回 agent 名 (string) 或 null
95
+ * 也兼容历史的"模板 X"写法以免老用户失忆。 */
110
96
  function tryExtractTemplateHint(text) {
111
- const m = text.match(/(?:用|使用)?\s*[「『"]?([^」』"\s,,;;]+)[」』"]?\s*模板/)
97
+ const m = text.match(/(?:用|使用)?\s*[「『"]?([^」』"\s,,;;]+)[」』"]?\s*(?:agent|员工|模板)/i)
112
98
  if (m) return m[1].trim()
113
- const m2 = text.match(/模板[::=\s]+([^\s,,;;]+)/)
99
+ const m2 = text.match(/(?:agent|员工|模板)[::=\s]+([^\s,,;;]+)/i)
114
100
  if (m2) return m2[1].trim()
115
101
  return null
116
102
  }
@@ -143,11 +129,11 @@ function extractTitle(text) {
143
129
  s = s.replace(/^(新建|开个|开一?个|创建)\s*(任务|todo)?[::\s]*/i, '')
144
130
  s = s.replace(/^任务[::]\s*/, '')
145
131
  s = s.replace(/^(帮我|帮忙)\s*(做|搞|修|搞定|实现|写一?个|做一?个|修复|重构|调试|debug|加|开发)\s*[::]?\s*/i, '')
146
- // 剥后缀(目录 / 象限 / 模板)
132
+ // 剥后缀(目录 / agent / 兼容老用户的"象限 N"残余字符)
147
133
  s = s.replace(/[,,;;]?\s*(目录|路径|workdir|cwd|文件夹)[::=\s]+[^\s,,;;]+/gi, '')
148
- s = s.replace(/[,,;;]?\s*(象限|quadrant|q)[::=\s]*[1-4]\b/gi, '')
149
- s = s.replace(/[,,;;]?\s*(?:用|使用)?\s*[「『"]?[^」』"\s,,;;]+[」』"]?\s*模板/gi, '')
150
- s = s.replace(/[,,;;]?\s*模板[::=\s]+[^\s,,;;]+/gi, '')
134
+ s = s.replace(/[,,;;]?\s*(象限|quadrant|q)[::=\s]*[1-4]\b/gi, '') // 象限概念已移除,但清掉残留写法
135
+ s = s.replace(/[,,;;]?\s*(?:用|使用)?\s*[「『"]?[^」』"\s,,;;]+[」』"]?\s*(?:agent|员工|模板)/gi, '')
136
+ s = s.replace(/[,,;;]?\s*(?:agent|员工|模板)[::=\s]+[^\s,,;;]+/gi, '')
151
137
  return s.trim()
152
138
  }
153
139
 
@@ -165,20 +151,12 @@ function buildWorkdirMessage(options) {
165
151
  return lines.join('\n')
166
152
  }
167
153
 
168
- function buildQuadrantMessage() {
169
- const lines = ['🎯 选象限:']
170
- QUADRANTS.forEach((q) => {
171
- lines.push(`${q.id}. ${q.label}${q.id === 2 ? ' ✓ 默认' : ''}`)
172
- })
173
- return lines.join('\n')
174
- }
175
-
176
154
  function buildTemplateMessage(templates) {
177
- const lines = ['📋 选模板:']
155
+ const lines = ['👤 派哪个 Agent 干?']
178
156
  templates.forEach((t, i) => {
179
157
  lines.push(`${i + 1}. ${t.name}${t.description ? ' — ' + t.description : ''}`)
180
158
  })
181
- lines.push(`${templates.length + 1}. 自由模式(不套模板)`)
159
+ lines.push(`${templates.length + 1}. 自由模式(不指派 agent)`)
182
160
  return lines.join('\n')
183
161
  }
184
162
 
@@ -186,8 +164,7 @@ function buildTemplateMessage(templates) {
186
164
  //
187
165
  // callback_data 编码(≤ 64 字节硬限):
188
166
  // workdir: qt:wd:<idx> / qt:wd:custom
189
- // quadrant: qt:q:<1..4>
190
- // template: qt:t:<idx> / qt:t:none
167
+ // template: qt:t:<idx> / qt:t:none (用户面叫 "agent")
191
168
  //
192
169
  // 数字按钮 label 沿用 "1. xxx" 序号 —— 跟纯文本 prompt 保持一致,
193
170
  // 所以双轨用户(按按钮的 / 回数字的)看到的是同一个心智模型。
@@ -224,25 +201,8 @@ function buildWorkdirReplyMarkup(options) {
224
201
  return { inline_keyboard: rows }
225
202
  }
226
203
 
227
- function buildQuadrantReplyMarkup() {
228
- // 2×2,Q2 默认勾上
229
- const label = (q) => `Q${q.id} ${q.label}${q.id === 2 ? ' ✓' : ''}`
230
- return {
231
- inline_keyboard: [
232
- [
233
- { text: label(QUADRANTS[0]), callback_data: `${CALLBACK_PREFIX}:q:1` },
234
- { text: label(QUADRANTS[1]), callback_data: `${CALLBACK_PREFIX}:q:2` },
235
- ],
236
- [
237
- { text: label(QUADRANTS[2]), callback_data: `${CALLBACK_PREFIX}:q:3` },
238
- { text: label(QUADRANTS[3]), callback_data: `${CALLBACK_PREFIX}:q:4` },
239
- ],
240
- ],
241
- }
242
- }
243
-
244
204
  function buildTemplateReplyMarkup(templates) {
245
- // 模板名也可能带描述 → 每行 1 个稳妥
205
+ // 名称可能带描述 → 每行 1 个稳妥
246
206
  const rows = templates.map((t, i) => [{
247
207
  text: `${i + 1}. ${ellipsisLabel(t.name, 48)}`,
248
208
  callback_data: `${CALLBACK_PREFIX}:t:${i}`,
@@ -258,10 +218,6 @@ function buildWorkdirPrompt(options) {
258
218
  return { text: buildWorkdirMessage(options), replyMarkup: buildWorkdirReplyMarkup(options) }
259
219
  }
260
220
 
261
- function buildQuadrantPrompt() {
262
- return { text: buildQuadrantMessage(), replyMarkup: buildQuadrantReplyMarkup() }
263
- }
264
-
265
221
  function buildTemplatePrompt(templates) {
266
222
  return { text: buildTemplateMessage(templates), replyMarkup: buildTemplateReplyMarkup(templates) }
267
223
  }
@@ -419,7 +375,6 @@ export function createOpenClawWizard({
419
375
  const routeKey = makeRouteKey(channel, chatId, threadId)
420
376
  const title = extractTitle(text) || '(未命名任务)'
421
377
  const workdirHint = tryExtractWorkdir(text)
422
- const quadrantHint = tryExtractQuadrant(text)
423
378
  const templateHint = tryExtractTemplateHint(text)
424
379
 
425
380
  const w = {
@@ -435,23 +390,21 @@ export function createOpenClawWizard({
435
390
  title,
436
391
  workdirOptions: listWorkdirOptions(),
437
392
  chosenWorkdir: workdirHint || null,
438
- chosenQuadrant: quadrantHint || null,
439
393
  chosenTemplate: null,
440
394
  step: STEP_WORKDIR,
441
395
  startedAt: Date.now(),
442
396
  updatedAt: Date.now(),
443
397
  }
444
398
 
445
- // 模板 hint 解析
399
+ // agent (template) hint 解析
446
400
  if (templateHint) {
447
401
  const tpl = findTemplateByHint(db.listTemplates(), templateHint)
448
402
  if (tpl) w.chosenTemplate = { id: tpl.id, name: tpl.name }
449
403
  }
450
404
 
451
- // 自动跳过已填字段
452
- if (w.chosenWorkdir) w.step = STEP_QUADRANT
453
- if (w.chosenWorkdir && w.chosenQuadrant) w.step = STEP_TEMPLATE
454
- if (w.chosenWorkdir && w.chosenQuadrant && w.chosenTemplate) w.step = STEP_DONE
405
+ // 自动跳过已填字段(不再有 quadrant 步骤)
406
+ if (w.chosenWorkdir) w.step = STEP_TEMPLATE
407
+ if (w.chosenWorkdir && w.chosenTemplate) w.step = STEP_DONE
455
408
 
456
409
  wizards.set(routeKey, w)
457
410
  return w
@@ -480,8 +433,10 @@ export function createOpenClawWizard({
480
433
  if (!path) return { reply: '🖋 路径为空,请重发' }
481
434
  w.chosenWorkdir = path
482
435
  w.awaitingCustomWorkdir = false
483
- w.step = STEP_QUADRANT
484
- const prompt = buildQuadrantPrompt()
436
+ w.step = STEP_TEMPLATE
437
+ const templates = db.listTemplates()
438
+ w.cachedTemplates = templates
439
+ const prompt = buildTemplatePrompt(templates)
485
440
  return { reply: prompt.text, replyMarkup: prompt.replyMarkup }
486
441
  }
487
442
  // 数字选项?
@@ -489,8 +444,10 @@ export function createOpenClawWizard({
489
444
  if (idx !== null) {
490
445
  if (idx < w.workdirOptions.length) {
491
446
  w.chosenWorkdir = w.workdirOptions[idx].path
492
- w.step = STEP_QUADRANT
493
- const prompt = buildQuadrantPrompt()
447
+ w.step = STEP_TEMPLATE
448
+ const templates = db.listTemplates()
449
+ w.cachedTemplates = templates
450
+ const prompt = buildTemplatePrompt(templates)
494
451
  return { reply: prompt.text, replyMarkup: prompt.replyMarkup }
495
452
  } else {
496
453
  // 选了"自定义"
@@ -500,8 +457,10 @@ export function createOpenClawWizard({
500
457
  // 自定义路径(文本路径下的隐式触发,老行为)
501
458
  if (text.startsWith('/') || text.startsWith('~')) {
502
459
  w.chosenWorkdir = text.trim()
503
- w.step = STEP_QUADRANT
504
- const prompt = buildQuadrantPrompt()
460
+ w.step = STEP_TEMPLATE
461
+ const templates = db.listTemplates()
462
+ w.cachedTemplates = templates
463
+ const prompt = buildTemplatePrompt(templates)
505
464
  return { reply: prompt.text, replyMarkup: prompt.replyMarkup }
506
465
  }
507
466
  // 看不懂 → 重发提示(保留按钮)
@@ -512,28 +471,7 @@ export function createOpenClawWizard({
512
471
  }
513
472
  }
514
473
 
515
- // ─── quadrant 步 ───
516
- if (w.step === STEP_QUADRANT) {
517
- const num = String(text).trim().match(/^([1-4])$/)
518
- if (num) {
519
- w.chosenQuadrant = Number(num[1])
520
- } else if (/默认|default|^$/i.test(text.trim())) {
521
- w.chosenQuadrant = 2
522
- } else {
523
- const prompt = buildQuadrantPrompt()
524
- return {
525
- reply: `🤔 请点按钮或回 1-4 选象限,回 "默认" 用 Q2。\n\n${prompt.text}`,
526
- replyMarkup: prompt.replyMarkup,
527
- }
528
- }
529
- w.step = STEP_TEMPLATE
530
- const templates = db.listTemplates()
531
- w.cachedTemplates = templates
532
- const prompt = buildTemplatePrompt(templates)
533
- return { reply: prompt.text, replyMarkup: prompt.replyMarkup }
534
- }
535
-
536
- // ─── template 步 ───
474
+ // ─── template (agent) 步 ───
537
475
  if (w.step === STEP_TEMPLATE) {
538
476
  const templates = w.cachedTemplates || db.listTemplates()
539
477
  const idx = parseNumericChoice(text, templates.length + 1)
@@ -554,7 +492,7 @@ export function createOpenClawWizard({
554
492
  } else {
555
493
  const prompt = buildTemplatePrompt(templates)
556
494
  return {
557
- reply: `🤔 请点按钮 / 回数字 1-${templates.length + 1},或模板名(自由/无)。\n\n${prompt.text}`,
495
+ reply: `🤔 请点按钮 / 回数字 1-${templates.length + 1},或 agent 名字(自由/无)。\n\n${prompt.text}`,
558
496
  replyMarkup: prompt.replyMarkup,
559
497
  }
560
498
  }
@@ -568,10 +506,9 @@ export function createOpenClawWizard({
568
506
 
569
507
  async function finalizeWizard(w) {
570
508
  try {
571
- // 创建 todo
509
+ // 创建 todo(quadrant 字段保留在 DB 里只是为了向后兼容;UI 不再展示)
572
510
  const todo = db.createTodo({
573
511
  title: w.title,
574
- quadrant: w.chosenQuadrant || 2,
575
512
  description: '',
576
513
  workDir: w.chosenWorkdir || null,
577
514
  brainstorm: false,
@@ -621,7 +558,7 @@ export function createOpenClawWizard({
621
558
  `${topicName}`,
622
559
  `AI 已启动,后续输出会回复在这个话题里。`,
623
560
  ``,
624
- `象限 Q${w.chosenQuadrant || 2} · 目录 ${w.chosenWorkdir || '默认'} · 模板 ${w.chosenTemplate?.name || '自由模式'}`,
561
+ `Agent ${w.chosenTemplate?.name || '自由模式'} · 目录 ${w.chosenWorkdir || '默认'}`,
625
562
  ].join('\n')
626
563
  // 复用用户当前所在话题 thread:把 intro 作为 thread reply 发进去,PTY 后续输出
627
564
  // 也用同一个 anchor 进同一话题,不再开新 thread。
@@ -770,7 +707,7 @@ export function createOpenClawWizard({
770
707
  const welcome = [
771
708
  `🤖 任务「${w.title}」AI 已启动 (${sessionInfo?.tool || 'claude'})`,
772
709
  ``,
773
- `象限 Q${w.chosenQuadrant || 2} · 目录 ${w.chosenWorkdir || '默认'} · 模板 ${w.chosenTemplate?.name || '自由模式'}`,
710
+ `Agent ${w.chosenTemplate?.name || '自由模式'} · 目录 ${w.chosenWorkdir || '默认'}`,
774
711
  ``,
775
712
  `AI 一轮回话/卡住/结束会推到这里。直接回任意文本会写进 PTY stdin。`,
776
713
  ].join('\n')
@@ -787,13 +724,12 @@ export function createOpenClawWizard({
787
724
  const lines = []
788
725
  if (createdThreadId) {
789
726
  lines.push(`✅ todo #${shortCode} 已建 → 去 topic 「${topicName}」 看进度`)
790
- lines.push(` Q${w.chosenQuadrant || 2} · ${w.chosenWorkdir || '默认目录'} · ${w.chosenTemplate?.name || '自由模式'}`)
727
+ lines.push(` Agent ${w.chosenTemplate?.name || '自由模式'} · ${w.chosenWorkdir || '默认目录'}`)
791
728
  } else {
792
729
  lines.push(`✅ todo #${shortCode} 已建`)
793
730
  lines.push(` 标题: ${w.title}`)
794
- lines.push(` 象限: Q${w.chosenQuadrant || 2}`)
731
+ lines.push(` Agent: ${w.chosenTemplate?.name || '自由模式'}`)
795
732
  lines.push(` 目录: ${w.chosenWorkdir || '默认'}`)
796
- lines.push(` 模板: ${w.chosenTemplate?.name || '自由模式'}`)
797
733
  if (sessionInfo) {
798
734
  lines.push(`🤖 ${sessionInfo.tool} 终端已启动 (sessionId: ${sessionInfo.sessionId.slice(-8)})`)
799
735
  }
@@ -821,9 +757,9 @@ export function createOpenClawWizard({
821
757
  if (!sessionId || !todoId) return { ok: false, reason: 'missing_args' }
822
758
  if (!telegramBot?.createForumTopic) return { ok: false, reason: 'no_telegram_bot' }
823
759
 
824
- // 已有路由跳过
825
- const existing = openclaw?.resolveRoute?.(sessionId)
826
- if (existing && existing.threadId) return { ok: true, action: 'already_bound' }
760
+ // 已有 telegram 路由 跳过(per-channel resolveRoute;避免被 lark 路由误判)
761
+ const existing = openclaw?.resolveRoute?.(sessionId, 'telegram')
762
+ if (existing?.threadId) return { ok: true, action: 'already_bound' }
827
763
 
828
764
  // DB 里已持久化(rehydrate 时常见)→ 重注路由就行
829
765
  const todo = db.getTodo(todoId)
@@ -896,8 +832,9 @@ export function createOpenClawWizard({
896
832
  if (!sessionId || !todoId) return { ok: false, reason: 'missing_args' }
897
833
  if (!larkBot?.sendMessage) return { ok: false, reason: 'no_lark_bot' }
898
834
 
899
- const existing = openclaw?.resolveRoute?.(sessionId)
900
- if (existing?.channel === 'lark' && existing?.rootMessageId) {
835
+ // 显式取 lark 路由,避免被 telegram 路由误判
836
+ const existing = openclaw?.resolveRoute?.(sessionId, 'lark')
837
+ if (existing?.rootMessageId) {
901
838
  return { ok: true, action: 'already_bound' }
902
839
  }
903
840
 
@@ -916,7 +853,7 @@ export function createOpenClawWizard({
916
853
 
917
854
  const shortCode = String(todoId).replace(/[^a-zA-Z0-9]/g, '').slice(-4).toLowerCase() || 'auto'
918
855
  const title = (todo.title || `todo-${shortCode}`).slice(0, 96)
919
- const topicName = `#t${shortCode} ${title}`.slice(0, 128)
856
+ const topicName = title.slice(0, 128)
920
857
  const intro = [
921
858
  `${topicName}`,
922
859
  `AI 已启动(自动镜像 from web/CLI),后续输出会回复在这条消息的 thread 里。`,
@@ -1015,7 +952,7 @@ export function createOpenClawWizard({
1015
952
  const todo = db.getTodo(sess.todoId)
1016
953
  if (todo) {
1017
954
  // 构造一个假 aiSession(DB 里没持久化,能跑就行)
1018
- const route = openclaw.resolveRoute?.(sid) || {}
955
+ const route = openclaw.resolveRoute?.(sid, 'telegram') || {}
1019
956
  const fakeAi = {
1020
957
  sessionId: sid,
1021
958
  tool: sess.tool,
@@ -1168,7 +1105,7 @@ export function createOpenClawWizard({
1168
1105
  if (sess?.todoId) {
1169
1106
  const todo = db.getTodo(sess.todoId)
1170
1107
  if (todo) {
1171
- const route = openclaw.resolveRoute?.(sid) || {}
1108
+ const route = openclaw.resolveRoute?.(sid, 'lark') || {}
1172
1109
  const fakeAi = {
1173
1110
  sessionId: sid,
1174
1111
  tool: sess.tool,
@@ -1272,6 +1209,16 @@ export function createOpenClawWizard({
1272
1209
  const routeKey = makeRouteKey(channel, chatId, threadId)
1273
1210
  const isLarkThreadReply = channel === 'lark' && (threadId || rootMessageId)
1274
1211
 
1212
+ // 飞书无前缀建任务守门:channel + 配置 + 文本 + slash 守门
1213
+ // 调用方需自行加 newTaskGateOpen + targetSid 缺失等额外条件。
1214
+ function shouldLarkAutoCreate() {
1215
+ if (channel !== 'lark') return false
1216
+ if (getConfig?.()?.lark?.autoCreateTodo === false) return false
1217
+ if (!trimmed) return false
1218
+ if (/^\/[a-z][a-z0-9_]*\b/i.test(trimmed)) return false
1219
+ return true
1220
+ }
1221
+
1275
1222
  // Lark 任务话题/root 回复必须严格隔离到原始路由:不允许被全局 ask_user、新任务触发词、
1276
1223
  // lastPush 或单活跃 session 等模糊 fallback 消费,避免把群内任务线程回复送到不相关会话。
1277
1224
  //
@@ -1455,15 +1402,11 @@ export function createOpenClawWizard({
1455
1402
  wizards.delete(routeKey)
1456
1403
  const w = startWizard({ channel, chatId, threadId, text: trimmed, messageId, rootMessageId, imagePaths, userId: fromUserId })
1457
1404
  if (w.step === STEP_DONE) return await finalizeWizard(w)
1458
- if (w.step === STEP_QUADRANT) {
1459
- const p = buildQuadrantPrompt()
1460
- return { reply: `(已重启向导,跳过目录步)\n${p.text}`, replyMarkup: p.replyMarkup }
1461
- }
1462
1405
  if (w.step === STEP_TEMPLATE) {
1463
1406
  const tpls = db.listTemplates()
1464
1407
  w.cachedTemplates = tpls
1465
1408
  const p = buildTemplatePrompt(tpls)
1466
- return { reply: `(已重启向导,跳过目录+象限步)\n${p.text}`, replyMarkup: p.replyMarkup }
1409
+ return { reply: `(已重启向导,跳过目录步)\n${p.text}`, replyMarkup: p.replyMarkup }
1467
1410
  }
1468
1411
  const p = buildWorkdirPrompt(w.workdirOptions)
1469
1412
  return { reply: `(已重启向导)\n${p.text}`, replyMarkup: p.replyMarkup, action: 'wizard_started' }
@@ -1478,20 +1421,12 @@ export function createOpenClawWizard({
1478
1421
  if (newTaskGateOpen && NEW_TASK_TRIGGERS.some((re) => re.test(trimmed))) {
1479
1422
  const w = startWizard({ channel, chatId, threadId, text: trimmed, messageId, rootMessageId, imagePaths, userId: fromUserId })
1480
1423
  if (w.step === STEP_DONE) return await finalizeWizard(w)
1481
- if (w.step === STEP_QUADRANT) {
1482
- const p = buildQuadrantPrompt()
1483
- return {
1484
- reply: `任务: ${w.title}\n(目录已识别为 ${w.chosenWorkdir})\n\n${p.text}`,
1485
- replyMarkup: p.replyMarkup,
1486
- action: 'wizard_started',
1487
- }
1488
- }
1489
1424
  if (w.step === STEP_TEMPLATE) {
1490
1425
  const tpls = db.listTemplates()
1491
1426
  w.cachedTemplates = tpls
1492
1427
  const p = buildTemplatePrompt(tpls)
1493
1428
  return {
1494
- reply: `任务: ${w.title}\n(目录+象限已识别)\n\n${p.text}`,
1429
+ reply: `任务: ${w.title}\n(目录已识别为 ${w.chosenWorkdir || '默认'})\n\n${p.text}`,
1495
1430
  replyMarkup: p.replyMarkup,
1496
1431
  action: 'wizard_started',
1497
1432
  }
@@ -1565,6 +1500,28 @@ export function createOpenClawWizard({
1565
1500
  }
1566
1501
  }
1567
1502
  if (targetSid && typeof targetSid === 'object' && targetSid.notFound) {
1503
+ // 未绑定 lark thread 的首条消息:默认起新建任务向导(受 autoCreateTodo 控制)
1504
+ if (newTaskGateOpen && shouldLarkAutoCreate()) {
1505
+ logger.info?.(`[wizard] lark auto-create from non-prefix text (unbound thread): chatId=${chatId} thread=${threadId || '-'} title="${trimmed.slice(0, 80)}"`)
1506
+ const w = startWizard({ channel, chatId, threadId, text: trimmed, messageId, rootMessageId, imagePaths, userId: fromUserId })
1507
+ if (w.step === STEP_DONE) return await finalizeWizard(w)
1508
+ if (w.step === STEP_TEMPLATE) {
1509
+ const tpls = db.listTemplates()
1510
+ w.cachedTemplates = tpls
1511
+ const p = buildTemplatePrompt(tpls)
1512
+ return {
1513
+ reply: `任务: ${w.title}\n(目录已识别为 ${w.chosenWorkdir || '默认'})\n\n${p.text}`,
1514
+ replyMarkup: p.replyMarkup,
1515
+ action: 'wizard_started',
1516
+ }
1517
+ }
1518
+ const p = buildWorkdirPrompt(w.workdirOptions)
1519
+ return {
1520
+ reply: `任务: ${w.title}\n\n${p.text}`,
1521
+ replyMarkup: p.replyMarkup,
1522
+ action: 'wizard_started',
1523
+ }
1524
+ }
1568
1525
  return {
1569
1526
  reply: '没有找到对应运行中的任务',
1570
1527
  action: 'session_not_found',
@@ -1718,6 +1675,30 @@ export function createOpenClawWizard({
1718
1675
  }
1719
1676
  }
1720
1677
 
1678
+ // 5.5 飞书无前缀建任务兜底:lark + autoCreateTodo + step 5 没匹配任何 PTY target →
1679
+ // 把消息原文当 title 起 wizard。Telegram/微信/openclaw 不受影响(channel 守门)。
1680
+ if (newTaskGateOpen && shouldLarkAutoCreate()) {
1681
+ logger.info?.(`[wizard] lark auto-create from non-prefix text: chatId=${chatId} thread=${threadId || '-'} title="${trimmed.slice(0, 80)}"`)
1682
+ const w = startWizard({ channel, chatId, threadId, text: trimmed, messageId, rootMessageId, imagePaths, userId: fromUserId })
1683
+ if (w.step === STEP_DONE) return await finalizeWizard(w)
1684
+ if (w.step === STEP_TEMPLATE) {
1685
+ const tpls = db.listTemplates()
1686
+ w.cachedTemplates = tpls
1687
+ const p = buildTemplatePrompt(tpls)
1688
+ return {
1689
+ reply: `任务: ${w.title}\n(目录已识别为 ${w.chosenWorkdir || '默认'})\n\n${p.text}`,
1690
+ replyMarkup: p.replyMarkup,
1691
+ action: 'wizard_started',
1692
+ }
1693
+ }
1694
+ const p = buildWorkdirPrompt(w.workdirOptions)
1695
+ return {
1696
+ reply: `任务: ${w.title}\n\n${p.text}`,
1697
+ replyMarkup: p.replyMarkup,
1698
+ action: 'wizard_started',
1699
+ }
1700
+ }
1701
+
1721
1702
  // 6. fallback
1722
1703
  // General 频道里专门提示:保护 PTY 上下文不被污染
1723
1704
  if (isInGeneralOfSupergroup) {
@@ -1757,9 +1738,8 @@ export function createOpenClawWizard({
1757
1738
  * callback_data 协议:
1758
1739
  * qt:wd:<idx> 工作目录(按 listWorkdirOptions 顺序)
1759
1740
  * qt:wd:custom 自定义路径 → 进入 awaitingCustomWorkdir 子态,等下一条文本
1760
- * qt:q:<1..4> 象限
1761
- * qt:t:<idx> 模板
1762
- * qt:t:none 自由模式(不套模板)
1741
+ * qt:t:<idx> agent(template;保留 t 前缀以避免破坏老的飞书卡片)
1742
+ * qt:t:none 自由模式(不指派 agent)
1763
1743
  *
1764
1744
  * 安全:所有未知 / 不匹配当前 step 的 callback 都返回 toast,从不 throw —— 让
1765
1745
  * telegram-bot 始终能 answerCallbackQuery 关 loading。
@@ -1782,7 +1762,7 @@ export function createOpenClawWizard({
1782
1762
  // ── 权限按钮路径(qt:perm:<short>:allow|deny)──────────────────
1783
1763
  const permCb = parsePermissionCallback(callbackData)
1784
1764
  if (permCb) {
1785
- return handlePermissionCallback(permCb, { chatId, threadId })
1765
+ return handlePermissionCallback(permCb, { chatId, threadId, channel: args.channel || null })
1786
1766
  }
1787
1767
  if (callbackData.startsWith(`${CALLBACK_PREFIX}:${PERMISSION_CALLBACK_KIND}:`)) {
1788
1768
  return { toast: '无效的权限按钮', action: 'invalid', editOriginal: true }
@@ -1835,8 +1815,10 @@ export function createOpenClawWizard({
1835
1815
  return { toast: '选项无效', action: 'invalid' }
1836
1816
  }
1837
1817
  w.chosenWorkdir = w.workdirOptions[idx].path
1838
- w.step = STEP_QUADRANT
1839
- const prompt = buildQuadrantPrompt()
1818
+ w.step = STEP_TEMPLATE
1819
+ const templates = db.listTemplates()
1820
+ w.cachedTemplates = templates
1821
+ const prompt = buildTemplatePrompt(templates)
1840
1822
  return {
1841
1823
  chosenLabel: w.chosenWorkdir,
1842
1824
  reply: prompt.text,
@@ -1845,31 +1827,19 @@ export function createOpenClawWizard({
1845
1827
  }
1846
1828
  }
1847
1829
 
1848
- // ── quadrant step ─────────────────────────────
1830
+ // ── 已退役:qt:q (quadrant) callback。老飞书/Telegram 卡片仍可能携带 → 友好提示 ──
1849
1831
  if (kind === 'q') {
1850
- if (w.step !== STEP_QUADRANT) {
1851
- return { toast: '当前步骤不接受象限选择', action: 'invalid' }
1852
- }
1853
- const q = Number(value)
1854
- if (![1, 2, 3, 4].includes(q)) return { toast: '象限无效', action: 'invalid' }
1855
- w.chosenQuadrant = q
1856
- w.step = STEP_TEMPLATE
1857
- const templates = db.listTemplates()
1858
- w.cachedTemplates = templates
1859
- const prompt = buildTemplatePrompt(templates)
1860
- const qLabel = QUADRANTS.find((x) => x.id === q)?.label || ''
1861
1832
  return {
1862
- chosenLabel: `Q${q} ${qLabel}`.trim(),
1863
- reply: prompt.text,
1864
- replyMarkup: prompt.replyMarkup,
1865
- action: 'wizard_step',
1833
+ toast: '象限步骤已移除,直接选 agent 即可',
1834
+ action: 'deprecated_quadrant',
1835
+ editOriginal: true,
1866
1836
  }
1867
1837
  }
1868
1838
 
1869
- // ── template step ─────────────────────────────
1839
+ // ── template step (用户面叫 agent) ─────────────────────────────
1870
1840
  if (kind === 't') {
1871
1841
  if (w.step !== STEP_TEMPLATE) {
1872
- return { toast: '当前步骤不接受模板选择', action: 'invalid' }
1842
+ return { toast: '当前步骤不接受 agent 选择', action: 'invalid' }
1873
1843
  }
1874
1844
  const templates = w.cachedTemplates || db.listTemplates()
1875
1845
  let label
@@ -1879,7 +1849,7 @@ export function createOpenClawWizard({
1879
1849
  } else {
1880
1850
  const idx = Number(value)
1881
1851
  if (!Number.isInteger(idx) || idx < 0 || idx >= templates.length) {
1882
- return { toast: '模板无效', action: 'invalid' }
1852
+ return { toast: 'agent 无效', action: 'invalid' }
1883
1853
  }
1884
1854
  w.chosenTemplate = { id: templates[idx].id, name: templates[idx].name }
1885
1855
  label = templates[idx].name
@@ -1899,30 +1869,51 @@ export function createOpenClawWizard({
1899
1869
  }
1900
1870
 
1901
1871
  // ─── 权限按钮回调 ───────────────────────────────────────────────
1902
- async function handlePermissionCallback({ short, action } = {}, { chatId, threadId } = {}) {
1903
- const stale = () => ({
1904
- toast: '会话已结束',
1905
- reply: `⚠️ 会话已结束(#${short}),无法发送权限选择。`,
1906
- action: 'permission_session_stale',
1907
- editOriginal: true,
1908
- })
1872
+ async function handlePermissionCallback({ short, action } = {}, { chatId, threadId, channel = null } = {}) {
1873
+ const stale = (why) => {
1874
+ logger.warn?.(`[wizard/perm] stale short=${short} action=${action} channel=${channel || 'null'} chatId=${chatId || 'null'} threadId=${threadId || 'null'} reason=${why}`)
1875
+ return {
1876
+ toast: '会话已结束',
1877
+ reply: `⚠️ 会话已结束(#${short}),无法发送权限选择。`,
1878
+ action: 'permission_session_stale',
1879
+ editOriginal: true,
1880
+ }
1881
+ }
1909
1882
 
1910
1883
  const sid = openclaw?.findSessionByShortId?.(short) || null
1911
- if (!sid || !pty?.has?.(sid)) return stale()
1912
- const route = openclaw?.resolveRoute?.(sid) || null
1913
- const sameChat = route && String(route.targetUserId) === String(chatId)
1914
- const sameThread = (route?.threadId || null) === (threadId || null)
1884
+ if (!sid) return stale('no_sid_for_short')
1885
+ if (!pty?.has?.(sid)) return stale(`pty_no_session sid=${sid}`)
1886
+ // channel hint 关键:同一 session 经常同时绑 telegram + lark,resolveRoute(sid)
1887
+ // 不带 channel 时返回最后注册的 route(通常是 telegram),导致 lark 点击的 sameChat
1888
+ // 校验失败被误判为 stale。caller 已经知道点击来自哪个渠道,必须传过来。
1889
+ const route = openclaw?.resolveRoute?.(sid, channel) || null
1890
+ if (!route) return stale(`no_route sid=${sid} channel=${channel || 'null'}`)
1891
+ const sameChat = String(route.targetUserId) === String(chatId)
1892
+ // 飞书 card.action.trigger 事件实测经常不带 open_thread_id(即使卡片是在 thread
1893
+ // 里发的);用 sameThread 硬校验会把所有 lark click 卡成 stale。lark 渠道下
1894
+ // 信任 sameChat + shortid(已经锁到具体 session)就够了,threadId 只在 telegram
1895
+ // 渠道生效(topic 隔离才有意义)。
1896
+ const sameThread = route.channel === 'lark'
1897
+ ? true
1898
+ : (route.threadId || null) === (threadId || null)
1915
1899
  // 允许 telegram / lark 渠道的权限回调;老的"threadId 非空就算"留作 legacy 兜底。
1916
- const isRoutedChannel = route?.channel === 'telegram' || route?.channel === 'lark' || route?.threadId != null
1917
- if (!sameChat || !sameThread || !isRoutedChannel) return stale()
1900
+ const isRoutedChannel = route.channel === 'telegram' || route.channel === 'lark' || route.threadId != null
1901
+ if (!sameChat || !sameThread || !isRoutedChannel) {
1902
+ return stale(`route_mismatch sid=${sid} route=${JSON.stringify({channel: route.channel, targetUserId: route.targetUserId, threadId: route.threadId})} sameChat=${sameChat} sameThread=${sameThread} isRoutedChannel=${isRoutedChannel}`)
1903
+ }
1918
1904
 
1919
1905
  if (action === PERMISSION_ACTION_ALLOW) {
1920
1906
  try {
1921
1907
  pty.write(sid, '\r')
1922
1908
  } catch (e) {
1923
1909
  logger.warn?.(`[wizard] permission allow write failed: ${e.message}`)
1924
- return stale()
1910
+ return stale('write_failed_allow')
1925
1911
  }
1912
+ // Lark click 直写 PTY 绕过了 /api/ai-terminal/input 路径,web 端的
1913
+ // permissionPrompt + 卡片不会自动消失。手工调一下 awaitingReply(false) 让
1914
+ // ai-terminal 走 markSessionRunningAfterInput → 翻 running、清 permissionPrompt、
1915
+ // 广播 pending_cleared,前端 webview 那张"AI 等待授权"卡随之消失。
1916
+ try { aiTerminal?.markSessionAwaitingReply?.(sid, false) } catch { /* ignore */ }
1926
1917
  return {
1927
1918
  toast: '已发送 Enter',
1928
1919
  chosenLabel: '允许(Enter)',
@@ -1936,8 +1927,9 @@ export function createOpenClawWizard({
1936
1927
  pty.write(sid, '\x1b')
1937
1928
  } catch (e) {
1938
1929
  logger.warn?.(`[wizard] permission deny write failed: ${e.message}`)
1939
- return stale()
1930
+ return stale('write_failed_deny')
1940
1931
  }
1932
+ try { aiTerminal?.markSessionAwaitingReply?.(sid, false) } catch { /* ignore */ }
1941
1933
  return {
1942
1934
  toast: '已发送 Esc',
1943
1935
  chosenLabel: '拒绝/退出(Esc)',
@@ -2114,7 +2106,7 @@ export function createOpenClawWizard({
2114
2106
 
2115
2107
  /**
2116
2108
  * 找到所有活跃 AI session(status=running / idle / pending_confirm),返回带 todo 上下文的列表:
2117
- * [{ sid, short, status, lastOutputAt, todo: {id, title, workDir, quadrant} | null }]
2109
+ * [{ sid, short, status, lastOutputAt, todo: {id, title, workDir} | null }]
2118
2110
  *
2119
2111
  * 数据源:
2120
2112
  * - aiTerminal.sessions (in-memory PTY session map) —— 状态最准
@@ -2152,7 +2144,6 @@ export function createOpenClawWizard({
2152
2144
  id: t.id,
2153
2145
  title: t.title || '(未命名)',
2154
2146
  workDir: t.workDir || '',
2155
- quadrant: t.quadrant || 2,
2156
2147
  }
2157
2148
  }
2158
2149
  }
@@ -2205,7 +2196,7 @@ export function createOpenClawWizard({
2205
2196
  }
2206
2197
 
2207
2198
  /**
2208
- * /list 或 /pending —— 列未完成 todos,按象限分组。
2199
+ * /list 或 /pending —— 列未完成 todos(扁平显示,按 updated_at 倒序)。
2209
2200
  *
2210
2201
  * 输出限制:30 条;超出加"去 web 看"提示。
2211
2202
  * 状态显示:把 todo.aiSessions 里 running 的标 🟢,便于一眼看哪些任务在跑。
@@ -2225,29 +2216,18 @@ export function createOpenClawWizard({
2225
2216
  const activeSids = new Set(findActiveSessions().map((s) => s.sid))
2226
2217
  // dispatcher 排队信息:sid → queueSize(busy 期间累积的用户输入)
2227
2218
  const dispatcherDesc = sessionInputDispatcher?.describe?.() || { byId: {} }
2228
- const groups = new Map()
2229
- for (const q of QUADRANTS) groups.set(q.id, [])
2230
- for (const t of visible) {
2231
- const arr = groups.get(t.quadrant) || groups.get(2)
2232
- arr.push(t)
2233
- }
2234
2219
  const lines = [`📋 待办 (${todos.length}${todos.length > PAGE ? `, 仅显示前 ${PAGE}` : ''})`]
2235
- for (const q of QUADRANTS) {
2236
- const arr = groups.get(q.id)
2237
- if (!arr || arr.length === 0) continue
2238
- lines.push('')
2239
- lines.push(`Q${q.id} ${q.label}`)
2240
- for (const t of arr) {
2241
- const short = String(t.id).slice(0, 4)
2242
- const dirTag = t.workDir ? ${basename(t.workDir)}` : ''
2243
- const aiSessions = t.aiSessions || (t.aiSession ? [t.aiSession] : [])
2244
- const runningSid = aiSessions.find((s) => s?.sessionId && activeSids.has(s.sessionId))?.sessionId
2245
- const isRunning = !!runningSid
2246
- const statusTag = isRunning ? '🟢' : '·'
2247
- const queueSize = runningSid ? (dispatcherDesc.byId?.[runningSid]?.queueSize || 0) : 0
2248
- const queueTag = queueSize > 0 ? ` 📥${queueSize}` : ''
2249
- lines.push(` ${statusTag} ${short} ${t.title} ${dirTag}${queueTag}`)
2250
- }
2220
+ lines.push('')
2221
+ for (const t of visible) {
2222
+ const short = String(t.id).slice(0, 4)
2223
+ const dirTag = t.workDir ? `· ${basename(t.workDir)}` : ''
2224
+ const aiSessions = t.aiSessions || (t.aiSession ? [t.aiSession] : [])
2225
+ const runningSid = aiSessions.find((s) => s?.sessionId && activeSids.has(s.sessionId))?.sessionId
2226
+ const isRunning = !!runningSid
2227
+ const statusTag = isRunning ? '🟢' : '·'
2228
+ const queueSize = runningSid ? (dispatcherDesc.byId?.[runningSid]?.queueSize || 0) : 0
2229
+ const queueTag = queueSize > 0 ? ` 📥${queueSize}` : ''
2230
+ lines.push(` ${statusTag} ${short} ${t.title} ${dirTag}${queueTag}`)
2251
2231
  }
2252
2232
  if (todos.length > PAGE) {
2253
2233
  const port = (getConfig?.()?.port) || 5677
@@ -2424,15 +2404,12 @@ export function createOpenClawWizard({
2424
2404
  export const __test__ = {
2425
2405
  extractTitle,
2426
2406
  tryExtractWorkdir,
2427
- tryExtractQuadrant,
2428
2407
  tryExtractTemplateHint,
2429
2408
  parseNumericChoice,
2430
2409
  findTemplateByHint,
2431
2410
  buildWorkdirMessage,
2432
- buildQuadrantMessage,
2433
2411
  buildTemplateMessage,
2434
2412
  buildWorkdirReplyMarkup,
2435
- buildQuadrantReplyMarkup,
2436
2413
  buildTemplateReplyMarkup,
2437
2414
  CALLBACK_PREFIX,
2438
2415
  isGeneralChannel,