agentquad 0.4.7 → 0.4.9
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/README.md +4 -3
- package/dist-web/assets/index-01ijE-nK.css +32 -0
- package/dist-web/assets/{index-BEiPvgk7.js → index-BfgxcGPX.js} +408 -388
- package/dist-web/assets/logo-Cxw7XzHl.png +0 -0
- package/dist-web/favicon.png +0 -0
- package/dist-web/index.html +2 -2
- package/package.json +1 -1
- package/src/cli.js +1 -1
- package/src/db.js +44 -15
- package/src/export/todoMarkdown.js +1 -9
- package/src/mcp/tools/destructive/index.js +22 -16
- package/src/mcp/tools/openclaw/index.js +11 -15
- package/src/mcp/tools/read/index.js +7 -7
- package/src/mcp/tools/write/index.js +9 -6
- package/src/openclaw-hook.js +32 -2
- package/src/openclaw-wizard.js +67 -185
- package/src/prompt-render.js +0 -8
- package/src/routes/ai-terminal.js +30 -0
- package/src/routes/todos.js +8 -14
- package/src/stats/report.js +1 -6
- package/src/wiki/index.js +1 -1
- package/src/wiki/sources.js +0 -1
- package/dist-web/assets/index-qY2UiOW2.css +0 -32
- package/dist-web/assets/logo-D4DDtU-r.png +0 -0
|
Binary file
|
package/dist-web/favicon.png
CHANGED
|
Binary file
|
package/dist-web/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
7
7
|
<title>AgentQuad</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-BfgxcGPX.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-01ijE-nK.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -667,7 +667,7 @@ export async function runStart(cmdOpts = {}) {
|
|
|
667
667
|
const program = new Command()
|
|
668
668
|
program
|
|
669
669
|
.name('agentquad')
|
|
670
|
-
.description('Local
|
|
670
|
+
.description('Local status-board todo CLI with embedded Claude Code / Codex / Cursor terminal')
|
|
671
671
|
.version(loadPkgVersion())
|
|
672
672
|
|
|
673
673
|
program.command('start')
|
package/src/db.js
CHANGED
|
@@ -885,23 +885,28 @@ export function openDb(file = ':memory:') {
|
|
|
885
885
|
if (unboundOnly) where.push('tf.bound_todo_id IS NULL')
|
|
886
886
|
|
|
887
887
|
if (q && ftsAvailable) {
|
|
888
|
-
// trigram tokenizer 要求 ≥3 字才能走 MATCH;<3
|
|
888
|
+
// trigram tokenizer 要求 ≥3 字才能走 MATCH;<3 字没有 FTS 索引可用,必须 LIKE 全表扫。
|
|
889
|
+
// 扫 transcript_fts.content(每轮一行,几万行)在大库上要 5s 级;这里只扫 transcript_files.first_user_prompt
|
|
890
|
+
// (每会话一行,~1500 行,100x 体量缩水) — 短查询场景里用户基本只关心首轮命中,可以接受漏掉 turn 内的匹配。
|
|
889
891
|
if (q.length < 3) {
|
|
890
892
|
const like = `%${q.replace(/[\\%_]/g, s => '\\' + s)}%`
|
|
891
|
-
where.push(`tf.
|
|
893
|
+
where.push(`tf.first_user_prompt LIKE ? ESCAPE '\\'`)
|
|
892
894
|
params.push(like)
|
|
893
895
|
const whereSql = where.length ? `WHERE ${where.join(' AND ')}` : ''
|
|
894
896
|
const total = db.prepare(`SELECT COUNT(*) AS n FROM transcript_files tf ${whereSql}`).get(...params).n
|
|
895
897
|
const rows = db.prepare(`
|
|
896
|
-
SELECT tf.*, (
|
|
897
|
-
SELECT SUBSTR(content, MAX(1, INSTR(content, ?) - 16), 64)
|
|
898
|
-
FROM transcript_fts WHERE file_id = tf.id AND content LIKE ? ESCAPE '\\' LIMIT 1
|
|
899
|
-
) AS snippet
|
|
898
|
+
SELECT tf.*, SUBSTR(tf.first_user_prompt, MAX(1, INSTR(tf.first_user_prompt, ?) - 16), 64) AS snippet
|
|
900
899
|
FROM transcript_files tf
|
|
901
900
|
${whereSql}
|
|
902
901
|
ORDER BY tf.started_at DESC
|
|
903
902
|
LIMIT ? OFFSET ?
|
|
904
|
-
`).all(q,
|
|
903
|
+
`).all(q, ...params, limit, offset)
|
|
904
|
+
// SQLite FTS 的 snippet() 只能走 MATCH,<3 字路径没法用它包裹高亮,这里在 JS 里手工补 <mark>,
|
|
905
|
+
// 让前端 dangerouslySetInnerHTML 渲染出和长查询一致的高亮效果。
|
|
906
|
+
const markRe = new RegExp(q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi')
|
|
907
|
+
for (const row of rows) {
|
|
908
|
+
if (row.snippet) row.snippet = row.snippet.replace(markRe, m => `<mark>${m}</mark>`)
|
|
909
|
+
}
|
|
905
910
|
return { total, items: rows }
|
|
906
911
|
}
|
|
907
912
|
const ftsQuery = q.replace(/"/g, '""')
|
|
@@ -1116,49 +1121,72 @@ export function openDb(file = ':memory:') {
|
|
|
1116
1121
|
// for existing users on next restart.
|
|
1117
1122
|
const BUILTIN_TEMPLATE_SEEDS = [
|
|
1118
1123
|
{
|
|
1119
|
-
name: '
|
|
1124
|
+
name: '方案顾问(脑爆)',
|
|
1120
1125
|
description: '先脑爆方向,不急着动手',
|
|
1121
1126
|
content: '请先不要直接动手实现。先针对下面的任务 brainstorm:\n- 列出 2-3 种可选方案,说明优缺点\n- 指出风险点与需要用户拍板的关键决策\n- 明确验收标准\n\n在我确认方案后再进入实现。',
|
|
1122
1127
|
},
|
|
1123
1128
|
{
|
|
1124
|
-
name: '
|
|
1129
|
+
name: '全自动工程师(自动驾驶)',
|
|
1125
1130
|
description: '内心脑爆 → 自选最优 → 跑完 → 最后报告',
|
|
1126
1131
|
content: '按"自动驾驶"模式处理下面的任务,不要停下来问我。\n\n1. 先在心里 brainstorm 2-3 种实现思路,挑出最合理的一种,但不需要列出来征求我的同意。\n2. 直接执行:理解 → 实现 → 自测(跑相关测试 / 编译 / lint) → 提交。\n3. 遇到不可避免必须我决策的歧义点(例:要不要删数据、要不要对外发版),才停下来问;普通选型不要问。\n4. 跑完后输出一份三段式报告:\n - 变更摘要:改了什么、为什么这么改\n - 验证结果:跑了哪些自测、是否通过、有没有遗留\n - 仍需我确认的事项:列出来 / 没有就写"无"\n5. 我考虑过哪些方案、为什么选这个,请在"变更摘要"里用一两句说明。',
|
|
1127
1132
|
},
|
|
1128
1133
|
{
|
|
1129
|
-
name: '
|
|
1134
|
+
name: '守稳派开发(稳定优先)',
|
|
1130
1135
|
description: '改动面最小、不引新依赖、不动公共 API',
|
|
1131
1136
|
content: '本任务执行时请优先保证"稳定",含义:\n- 改动面尽量小,不顺手重构、不删看似无用的代码\n- 不引入新依赖、不升级现有依赖\n- 不改公共 API / 接口签名 / 数据库 schema(除非任务本身就是改这个)\n- 优先补测试覆盖现有行为;改动 hot path 时尽量保留旧路径作为兜底\n- 选型偏保守:用已经在项目里用过的库 / 写法\n如果"稳定"和任务目标冲突,告诉我,让我决定。',
|
|
1132
1137
|
},
|
|
1133
1138
|
{
|
|
1134
|
-
name: '
|
|
1139
|
+
name: '前瞻派架构师(未来发展优先)',
|
|
1135
1140
|
description: '允许重构 / 引入抽象 / 留扩展点',
|
|
1136
1141
|
content: '本任务执行时请优先考虑"长期可扩展性",含义:\n- 允许并鼓励顺手重构邻近代码,让结构更清晰\n- 可以引入新抽象 / 接口,为可预见的下一步需求留扩展点\n- 公共 API / 类型 / 数据结构允许调整,但要在报告里列出影响面(调用方 / 测试 / 文档)\n- 选型可以挑当前社区主流而非项目已有的旧写法,但要说明替换原因\n- 不要为完全假想的需求过度设计 —— 只服务"已经看到苗头"的下一步\n完成后在报告里说明:哪些是为长期留的扩展点,分别服务什么场景。',
|
|
1137
1142
|
},
|
|
1138
1143
|
{
|
|
1139
|
-
name: 'Bug
|
|
1144
|
+
name: 'Bug 侦探',
|
|
1140
1145
|
description: '复现 → 定位 → 最小用例 → 修复 → 回归',
|
|
1141
1146
|
content: '按 bug 修复流程处理下面的问题:\n1. 先复现(给出复现步骤和实际 vs 预期)\n2. 定位根因(不要过早修改代码)\n3. 写一个能复现该 bug 的最小用例(如果有测试框架)\n4. 修复根因,不是修现象\n5. 回归:跑相关测试;考虑同类 bug 是否还存在',
|
|
1142
1147
|
},
|
|
1143
1148
|
{
|
|
1144
|
-
name: '
|
|
1149
|
+
name: '重构师',
|
|
1145
1150
|
description: '先读懂 → 列出影响面 → 小步重构',
|
|
1146
1151
|
content: '按照小步重构原则处理下面的任务:\n1. 先通读相关代码,复述你的理解\n2. 列出此次重构的影响面(调用方 / 测试 / 类型)\n3. 每一步只改一件事,保持可运行\n4. 每步后跑一次测试(如果有)\n5. 不要顺手加功能、不要引入新抽象,除非当前任务要求',
|
|
1147
1152
|
},
|
|
1148
1153
|
{
|
|
1149
|
-
name: '
|
|
1154
|
+
name: '测试工程师',
|
|
1150
1155
|
description: 'TDD:红 → 绿 → 重构',
|
|
1151
1156
|
content: '用 TDD 的方式处理下面的任务:\n1. 先列出测试矩阵(输入 × 场景)\n2. 先写一个最简失败用例(红)\n3. 用最小改动让它通过(绿)\n4. 重构(保持绿)\n5. 重复 2-4 直到覆盖矩阵\n不 mock 真实依赖(除非跨网络/支付等)。',
|
|
1152
1157
|
},
|
|
1153
1158
|
{
|
|
1154
|
-
name: '
|
|
1159
|
+
name: '代码评审员',
|
|
1155
1160
|
description: '只评审,不改代码',
|
|
1156
1161
|
content: '请只做代码评审,不要修改代码。按下面的维度给出具体反馈:\n- 可读性:命名、结构、注释\n- 正确性:边界、错误处理、并发\n- 安全性:注入、鉴权、敏感数据\n- 性能:明显的 N+1 / 无谓复制\n- 简洁性:是否有过度设计 / 可删除的冗余\n每条反馈给出文件:行号 + 建议。',
|
|
1157
1162
|
},
|
|
1158
1163
|
]
|
|
1164
|
+
// 把老用户库里旧名字的 builtin 行就地改名成新名字,避免新种子重复插入。
|
|
1165
|
+
// 仅在新名尚未占位时改名;用户复制版(builtin=0)不动。
|
|
1166
|
+
const BUILTIN_RENAMES = [
|
|
1167
|
+
['Brainstorm(脑爆)', '方案顾问(脑爆)'],
|
|
1168
|
+
['自动驾驶(Autopilot)', '全自动工程师(自动驾驶)'],
|
|
1169
|
+
['稳定优先(Stability First)', '守稳派开发(稳定优先)'],
|
|
1170
|
+
['未来发展优先(Future First)', '前瞻派架构师(未来发展优先)'],
|
|
1171
|
+
['Bug 修复', 'Bug 侦探'],
|
|
1172
|
+
['重构', '重构师'],
|
|
1173
|
+
['写测试', '测试工程师'],
|
|
1174
|
+
['代码评审', '代码评审员'],
|
|
1175
|
+
]
|
|
1159
1176
|
const findBuiltinByName = db.prepare(
|
|
1160
1177
|
`SELECT id FROM prompt_templates WHERE builtin = 1 AND name = ? LIMIT 1`,
|
|
1161
1178
|
)
|
|
1179
|
+
const renameBuiltin = db.prepare(
|
|
1180
|
+
`UPDATE prompt_templates SET name = ?, updated_at = ? WHERE builtin = 1 AND name = ?`,
|
|
1181
|
+
)
|
|
1182
|
+
function migrateBuiltinNames() {
|
|
1183
|
+
const now = Date.now()
|
|
1184
|
+
for (const [oldName, newName] of BUILTIN_RENAMES) {
|
|
1185
|
+
if (findBuiltinByName.get(newName)) continue // 新名已存在则跳过
|
|
1186
|
+
if (!findBuiltinByName.get(oldName)) continue // 旧名也不在就没事
|
|
1187
|
+
renameBuiltin.run(newName, now, oldName)
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1162
1190
|
function ensureBuiltinTemplates() {
|
|
1163
1191
|
const now = Date.now()
|
|
1164
1192
|
BUILTIN_TEMPLATE_SEEDS.forEach((s, i) => {
|
|
@@ -1175,6 +1203,7 @@ export function openDb(file = ':memory:') {
|
|
|
1175
1203
|
})
|
|
1176
1204
|
})
|
|
1177
1205
|
}
|
|
1206
|
+
migrateBuiltinNames()
|
|
1178
1207
|
ensureBuiltinTemplates()
|
|
1179
1208
|
|
|
1180
1209
|
const wikiStmts = {
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import { parseTranscriptFile } from '../transcripts/scanner.js'
|
|
2
2
|
import { estimateCost, DEFAULT_PRICING } from '../pricing.js'
|
|
3
3
|
|
|
4
|
-
const QUADRANT_LABEL = {
|
|
5
|
-
1: 'Q1 紧急且重要',
|
|
6
|
-
2: 'Q2 重要不紧急',
|
|
7
|
-
3: 'Q3 紧急不重要',
|
|
8
|
-
4: 'Q4 不紧急不重要',
|
|
9
|
-
}
|
|
10
|
-
|
|
11
4
|
const STATUS_LABEL = {
|
|
12
5
|
todo: '待办',
|
|
13
6
|
ai_pending: 'AI 待确认',
|
|
@@ -30,7 +23,7 @@ export async function buildTodoExport(db, todoId, { turns = 'summary', turnLimit
|
|
|
30
23
|
if (!todo) return null
|
|
31
24
|
const comments = db.listComments(todoId)
|
|
32
25
|
const aiSessions = Array.isArray(todo.aiSessions) ? todo.aiSessions : []
|
|
33
|
-
const subtodos = todo.parentId ? [] : db.listTodos(
|
|
26
|
+
const subtodos = todo.parentId ? [] : db.listTodos().filter(item => item.parentId === todo.id)
|
|
34
27
|
|
|
35
28
|
const sessionRows = []
|
|
36
29
|
for (const s of aiSessions) {
|
|
@@ -122,7 +115,6 @@ export function renderTodoMarkdown(report) {
|
|
|
122
115
|
lines.push(`# ${todo.title}`)
|
|
123
116
|
lines.push('')
|
|
124
117
|
const meta = [
|
|
125
|
-
`**象限**:${QUADRANT_LABEL[todo.quadrant] || todo.quadrant}`,
|
|
126
118
|
`**状态**:${STATUS_LABEL[todo.status] || todo.status}`,
|
|
127
119
|
]
|
|
128
120
|
if (todo.dueDate) meta.push(`**截止**:${fmtDateTime(todo.dueDate)}`)
|
|
@@ -195,12 +195,15 @@ export function registerDestructiveTools(server, { db, audit }) {
|
|
|
195
195
|
'bulk_update',
|
|
196
196
|
{
|
|
197
197
|
description:
|
|
198
|
-
'对一组 todo id 批量 patch。允许字段:
|
|
198
|
+
'对一组 todo id 批量 patch。允许字段:status / archived / dueDate。' +
|
|
199
|
+
'(quadrant 字段保留接口以兼容旧调用方,传入会被忽略。)' +
|
|
200
|
+
'默认 confirm:false 返回 preview(列出将要修改的 todos,最多 20 条)。',
|
|
199
201
|
inputSchema: {
|
|
200
202
|
ids: z.array(z.string().min(1)).min(1),
|
|
201
203
|
patch: z
|
|
202
204
|
.object({
|
|
203
|
-
quadrant: z.number().int().min(1).max(4).optional()
|
|
205
|
+
quadrant: z.number().int().min(1).max(4).optional()
|
|
206
|
+
.describe('【已退役】象限概念已移除,传入会被忽略'),
|
|
204
207
|
status: z.enum(['todo', 'done']).optional(),
|
|
205
208
|
archived: z.boolean().optional(),
|
|
206
209
|
dueDate: z.number().int().nullable().optional(),
|
|
@@ -211,40 +214,43 @@ export function registerDestructiveTools(server, { db, audit }) {
|
|
|
211
214
|
},
|
|
212
215
|
},
|
|
213
216
|
async (args) => {
|
|
217
|
+
// 剥掉退役字段,避免 db 层试图写 quadrant
|
|
218
|
+
const { quadrant: _ignored, ...effectivePatch } = args.patch || {}
|
|
219
|
+
const sanitizedArgs = { ...args, patch: effectivePatch }
|
|
214
220
|
try {
|
|
215
221
|
const preview = {
|
|
216
|
-
ids:
|
|
217
|
-
patch:
|
|
222
|
+
ids: sanitizedArgs.ids,
|
|
223
|
+
patch: sanitizedArgs.patch,
|
|
218
224
|
affected: [],
|
|
219
225
|
missing: [],
|
|
220
226
|
}
|
|
221
|
-
for (const id of
|
|
227
|
+
for (const id of sanitizedArgs.ids.slice(0, 20)) {
|
|
222
228
|
const t = db.getTodo(id)
|
|
223
|
-
if (t) preview.affected.push({ id: t.id, title: t.title,
|
|
229
|
+
if (t) preview.affected.push({ id: t.id, title: t.title, status: t.status })
|
|
224
230
|
else preview.missing.push(id)
|
|
225
231
|
}
|
|
226
|
-
preview.totalTargeted =
|
|
227
|
-
preview.previewTruncated =
|
|
228
|
-
if (!
|
|
229
|
-
const patchKeys = Object.keys(
|
|
232
|
+
preview.totalTargeted = sanitizedArgs.ids.length
|
|
233
|
+
preview.previewTruncated = sanitizedArgs.ids.length > 20
|
|
234
|
+
if (!sanitizedArgs.confirm) {
|
|
235
|
+
const patchKeys = Object.keys(sanitizedArgs.patch).join(', ') || '(空 — quadrant 已退役)'
|
|
230
236
|
return previewResponse({
|
|
231
237
|
toolName: 'bulk_update',
|
|
232
|
-
summary: `将对 ${
|
|
238
|
+
summary: `将对 ${sanitizedArgs.ids.length} 条 todo 批量 patch 以下字段:${patchKeys}。命中 ${preview.affected.length} 条、未找到 ${preview.missing.length} 条。`,
|
|
233
239
|
impact: preview,
|
|
234
|
-
args,
|
|
240
|
+
args: sanitizedArgs,
|
|
235
241
|
})
|
|
236
242
|
}
|
|
237
|
-
const result = db.bulkUpdateTodos({ ids:
|
|
243
|
+
const result = db.bulkUpdateTodos({ ids: sanitizedArgs.ids, patch: sanitizedArgs.patch })
|
|
238
244
|
audit?.append({
|
|
239
245
|
tool: 'bulk_update',
|
|
240
246
|
ok: true,
|
|
241
|
-
args: { ids:
|
|
247
|
+
args: { ids: sanitizedArgs.ids, patch: sanitizedArgs.patch },
|
|
242
248
|
result: { changedCount: result.count, changedIds: result.changedIds },
|
|
243
|
-
confirmNote:
|
|
249
|
+
confirmNote: sanitizedArgs.confirmNote || null,
|
|
244
250
|
})
|
|
245
251
|
return asText({ ok: true, result })
|
|
246
252
|
} catch (e) {
|
|
247
|
-
audit?.append({ tool: 'bulk_update', ok: false, args: { ids:
|
|
253
|
+
audit?.append({ tool: 'bulk_update', ok: false, args: { ids: sanitizedArgs.ids, patch: sanitizedArgs.patch }, error: e?.message })
|
|
248
254
|
return asError(e?.message || 'bulk_update_failed')
|
|
249
255
|
}
|
|
250
256
|
},
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OpenClaw 双向桥接的 MCP
|
|
2
|
+
* OpenClaw 双向桥接的 MCP 工具集:
|
|
3
3
|
*
|
|
4
4
|
* 创建任务向导(OpenClaw skill 调):
|
|
5
5
|
* - list_workdir_options
|
|
6
|
-
* - list_quadrants
|
|
7
|
-
* - list_templates
|
|
6
|
+
* - list_quadrants (已废弃,仅为兼容旧 skill;返回空数组 + deprecated 标记)
|
|
7
|
+
* - list_templates (用户面叫 "agent / 员工",工具名保留向后兼容)
|
|
8
8
|
*
|
|
9
9
|
* 启动 PTY(OpenClaw skill 调):
|
|
10
10
|
* - start_ai_session
|
|
@@ -37,13 +37,6 @@ function asError(message, extra = null) {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
const QUADRANTS = [
|
|
41
|
-
{ id: 1, label: '重要紧急', shortLabel: 'Q1', isDefault: false },
|
|
42
|
-
{ id: 2, label: '重要不紧急', shortLabel: 'Q2', isDefault: true },
|
|
43
|
-
{ id: 3, label: '紧急不重要', shortLabel: 'Q3', isDefault: false },
|
|
44
|
-
{ id: 4, label: '不重要不紧急', shortLabel: 'Q4', isDefault: false },
|
|
45
|
-
]
|
|
46
|
-
|
|
47
40
|
function expandHome(p) {
|
|
48
41
|
if (!p) return p
|
|
49
42
|
if (p === '~' || p.startsWith('~/')) return p === '~' ? homedir() : join(homedir(), p.slice(2))
|
|
@@ -105,22 +98,25 @@ export function registerOpenClawTools(server, deps) {
|
|
|
105
98
|
},
|
|
106
99
|
)
|
|
107
100
|
|
|
108
|
-
// ─── 2. list_quadrants
|
|
101
|
+
// ─── 2. list_quadrants(已废弃;保留以兼容旧版 OpenClaw skill)──
|
|
109
102
|
server.registerTool(
|
|
110
103
|
'list_quadrants',
|
|
111
104
|
{
|
|
112
|
-
description:
|
|
105
|
+
description:
|
|
106
|
+
'【已废弃】象限概念已从 AgentQuad 移除。本工具仅为兼容旧 skill 而保留,' +
|
|
107
|
+
'返回空数组 + deprecated 标记。新创建任务无需再选象限。',
|
|
113
108
|
inputSchema: {},
|
|
114
109
|
},
|
|
115
|
-
async () => asText({ quadrants:
|
|
110
|
+
async () => asText({ quadrants: [], deprecated: true }),
|
|
116
111
|
)
|
|
117
112
|
|
|
118
|
-
// ─── 3. list_templates
|
|
113
|
+
// ─── 3. list_templates (用户面叫 agent / 员工) ─────────────────────
|
|
119
114
|
server.registerTool(
|
|
120
115
|
'list_templates',
|
|
121
116
|
{
|
|
122
117
|
description:
|
|
123
|
-
'
|
|
118
|
+
'返回所有可指派的 agent / 员工(id/name/description/builtin/contentPreview),' +
|
|
119
|
+
'创建向导选 agent 那一步用。' +
|
|
124
120
|
'完整 content 不返回(避免上下文爆);启动会话时按 templateId 自动注入。',
|
|
125
121
|
inputSchema: {},
|
|
126
122
|
},
|
|
@@ -53,7 +53,8 @@ export function registerReadTools(server, { db, searchService, wikiDir, transcri
|
|
|
53
53
|
description:
|
|
54
54
|
'按过滤条件列出 todos。不做全文搜索——用 search 做模糊搜索。返回元数据数组,不含 AI 会话详情(用 get_todo 取)。',
|
|
55
55
|
inputSchema: {
|
|
56
|
-
quadrant: z.number().int().min(1).max(4).optional()
|
|
56
|
+
quadrant: z.number().int().min(1).max(4).optional()
|
|
57
|
+
.describe('【已退役】象限概念已移除;保留参数用于兼容旧调用方,传入会被忽略'),
|
|
57
58
|
status: z.enum(['todo', 'done', 'all']).optional().describe('默认 all'),
|
|
58
59
|
archived: z.union([z.boolean(), z.literal('all')]).optional().describe('默认 false 只看未归档;"all" 两者都要'),
|
|
59
60
|
parentId: z.string().optional().describe('仅列某 parent 下的子任务'),
|
|
@@ -63,8 +64,8 @@ export function registerReadTools(server, { db, searchService, wikiDir, transcri
|
|
|
63
64
|
async (args = {}) => {
|
|
64
65
|
try {
|
|
65
66
|
const rawStatus = args.status === 'all' ? '' : args.status
|
|
67
|
+
// quadrant 已退役,不再透传 db 层
|
|
66
68
|
const list = db.listTodos({
|
|
67
|
-
quadrant: args.quadrant,
|
|
68
69
|
status: rawStatus,
|
|
69
70
|
archived: args.archived,
|
|
70
71
|
})
|
|
@@ -78,7 +79,6 @@ export function registerReadTools(server, { db, searchService, wikiDir, transcri
|
|
|
78
79
|
id: t.id,
|
|
79
80
|
parentId: t.parentId,
|
|
80
81
|
title: t.title,
|
|
81
|
-
quadrant: t.quadrant,
|
|
82
82
|
status: t.status,
|
|
83
83
|
dueDate: t.dueDate,
|
|
84
84
|
workDir: t.workDir,
|
|
@@ -151,7 +151,7 @@ export function registerReadTools(server, { db, searchService, wikiDir, transcri
|
|
|
151
151
|
'get_stats',
|
|
152
152
|
{
|
|
153
153
|
description:
|
|
154
|
-
'
|
|
154
|
+
'一个当前快照:按 todo 状态分布、今日截止、本周完成、最近 7 天活跃度、归档数量。轻量实时计算。',
|
|
155
155
|
inputSchema: {},
|
|
156
156
|
},
|
|
157
157
|
async () => {
|
|
@@ -162,15 +162,15 @@ export function registerReadTools(server, { db, searchService, wikiDir, transcri
|
|
|
162
162
|
const all = db.listTodos({ archived: 'all' })
|
|
163
163
|
const open = all.filter((t) => t.status !== 'done' && t.status !== 'missed' && t.archivedAt == null)
|
|
164
164
|
const archivedCount = all.filter((t) => t.archivedAt != null).length
|
|
165
|
-
const
|
|
166
|
-
for (const t of open)
|
|
165
|
+
const byStatus = {}
|
|
166
|
+
for (const t of open) byStatus[t.status] = (byStatus[t.status] || 0) + 1
|
|
167
167
|
const overdue = open.filter((t) => t.dueDate && t.dueDate < now)
|
|
168
168
|
const dueToday = open.filter((t) => t.dueDate && t.dueDate >= startOfDay.getTime() && t.dueDate < startOfDay.getTime() + 86_400_000)
|
|
169
169
|
const weekDone = db.listCompletedTodos({ since: startOfWeek.getTime(), until: now + 1 })
|
|
170
170
|
return asText({
|
|
171
171
|
openCount: open.length,
|
|
172
172
|
archivedCount,
|
|
173
|
-
|
|
173
|
+
byStatus,
|
|
174
174
|
overdue: { count: overdue.length, ids: overdue.slice(0, 10).map((t) => t.id) },
|
|
175
175
|
dueToday: { count: dueToday.length, ids: dueToday.map((t) => t.id) },
|
|
176
176
|
completedThisWeek: weekDone.length,
|
|
@@ -23,12 +23,14 @@ export function registerWriteTools(server, { db }) {
|
|
|
23
23
|
'create_todo',
|
|
24
24
|
{
|
|
25
25
|
description:
|
|
26
|
-
'新建一条 todo
|
|
26
|
+
'新建一条 todo。只需 title。' +
|
|
27
|
+
'(quadrant 字段已退役但接口仍接受以兼容旧调用方;传入会被忽略,DB 写入默认值。)',
|
|
27
28
|
inputSchema: {
|
|
28
29
|
title: z.string().min(1).describe('标题,必填'),
|
|
29
|
-
quadrant: z.number().int().min(1).max(4).
|
|
30
|
+
quadrant: z.number().int().min(1).max(4).optional()
|
|
31
|
+
.describe('【已退役】象限概念已移除,传入会被忽略'),
|
|
30
32
|
description: z.string().optional(),
|
|
31
|
-
parentId: z.string().optional().describe('
|
|
33
|
+
parentId: z.string().optional().describe('已退役:子任务概念已从 UI 移除;接口保留但不再分组展示'),
|
|
32
34
|
dueDate: z.number().int().optional().describe('截止时间戳(毫秒 epoch)'),
|
|
33
35
|
workDir: z.string().optional().describe('关联的代码仓路径'),
|
|
34
36
|
brainstorm: z.boolean().optional(),
|
|
@@ -39,7 +41,7 @@ export function registerWriteTools(server, { db }) {
|
|
|
39
41
|
if (!args.title?.trim()) return asError('title_required')
|
|
40
42
|
const created = db.createTodo({
|
|
41
43
|
title: args.title.trim(),
|
|
42
|
-
quadrant
|
|
44
|
+
// quadrant 不再透传 —— 让 db 层用默认值
|
|
43
45
|
description: args.description || '',
|
|
44
46
|
parentId: args.parentId,
|
|
45
47
|
dueDate: args.dueDate,
|
|
@@ -63,7 +65,8 @@ export function registerWriteTools(server, { db }) {
|
|
|
63
65
|
id: z.string().min(1),
|
|
64
66
|
title: z.string().optional(),
|
|
65
67
|
description: z.string().optional(),
|
|
66
|
-
quadrant: z.number().int().min(1).max(4).optional()
|
|
68
|
+
quadrant: z.number().int().min(1).max(4).optional()
|
|
69
|
+
.describe('【已退役】象限概念已移除,传入会被忽略'),
|
|
67
70
|
dueDate: z.number().int().nullable().optional().describe('传 null 显式清除'),
|
|
68
71
|
workDir: z.string().nullable().optional(),
|
|
69
72
|
parentId: z.string().nullable().optional().describe('传 null 升为顶层 todo'),
|
|
@@ -71,7 +74,7 @@ export function registerWriteTools(server, { db }) {
|
|
|
71
74
|
},
|
|
72
75
|
async (args) => {
|
|
73
76
|
try {
|
|
74
|
-
const { id, ...patch } = args
|
|
77
|
+
const { id, quadrant: _ignored, ...patch } = args
|
|
75
78
|
if (!id) return asError('id_required')
|
|
76
79
|
// 禁止通过此工具改 status,status 变更有专用工具(complete/archive)
|
|
77
80
|
const cleanPatch = {}
|
package/src/openclaw-hook.js
CHANGED
|
@@ -336,6 +336,22 @@ export function createOpenClawHookHandler(deps = {}) {
|
|
|
336
336
|
return getSessionPermissionMode(sessionId) !== 'bypass'
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
+
// detector 路径专用:bypass guard 让位。理由是 detector 三层守卫(anchor +
|
|
340
|
+
// ≥2 数字选项 + Esc/Tab footer)证明 Claude TUI 实际**正在**渲染权限框 ——
|
|
341
|
+
// 不管 AgentQuad 记的是不是 bypass。
|
|
342
|
+
// 真实场景:主人启动 session 时选了 bypass,之后在 TUI 内用 /permission-mode
|
|
343
|
+
// 切到 default。AgentQuad 没法感知 TUI 内部模式改动(hook / jsonl 都不 echo
|
|
344
|
+
// 这条信息),session.permissionMode 永远停在 'bypass'。如果继续按 bypass guard
|
|
345
|
+
// 拒推,前端能看见 "AI 等待授权" 卡片但 IM 永远收不到 —— 跟原始 bug 同症状。
|
|
346
|
+
// handleClaude (Notification hook) 仍走老 isPermissionReminderEligible,因为
|
|
347
|
+
// 那条路径在真 bypass 下 fire 多半是 idle 通知噪声,吞掉是对的。
|
|
348
|
+
function isDetectorPushEligible(sessionId) {
|
|
349
|
+
if (!sessionId) return false
|
|
350
|
+
if (!resolveExplicitInteractiveRoute(sessionId)) return false
|
|
351
|
+
if (suppressPermissionNotifications()) return false
|
|
352
|
+
return true
|
|
353
|
+
}
|
|
354
|
+
|
|
339
355
|
function permissionShortId(sessionId) {
|
|
340
356
|
return String(sessionId || '').slice(-4)
|
|
341
357
|
}
|
|
@@ -621,6 +637,11 @@ export function createOpenClawHookHandler(deps = {}) {
|
|
|
621
637
|
if (!sessionId) return { ok: false, reason: 'no_sessionId' }
|
|
622
638
|
const sess = aiTerminal?.sessions?.get(sessionId)
|
|
623
639
|
if (!sess) return { ok: false, reason: 'session_gone' }
|
|
640
|
+
// bridge in-memory route 缺失但 DB 有 → 先恢复,否则 postCard 一样发不出去。
|
|
641
|
+
// handleClaudeDetector 同源处理,保持两条 detector 路径行为一致。
|
|
642
|
+
if (openclaw?.hasExplicitRoute && !openclaw.hasExplicitRoute(sessionId)) {
|
|
643
|
+
restorePersistedRoute(sessionId, sess.todoId)
|
|
644
|
+
}
|
|
624
645
|
// 把 session.status 翻成 pending_confirm —— 前端 deriveAiState 据此显示"待确认"。
|
|
625
646
|
// 信号源是 codex-prompt-detector(已经过 AI self-quoted 过滤),比旧的 PTY 正则路径准。
|
|
626
647
|
try { aiTerminal?.markPendingConfirm?.(sessionId, { source: 'codex-detector', promptText }) } catch { /* ignore */ }
|
|
@@ -658,6 +679,14 @@ export function createOpenClawHookHandler(deps = {}) {
|
|
|
658
679
|
const sess = aiTerminal?.sessions?.get(sessionId)
|
|
659
680
|
if (!sess) return { ok: false, reason: 'session_gone' }
|
|
660
681
|
|
|
682
|
+
// 0) bridge in-memory route 缺失但 DB 有 → 先恢复。否则 isPermissionReminderEligible
|
|
683
|
+
// 会立即 short-circuit 返回 false,IM 永远收不到权限卡片。
|
|
684
|
+
// handleClaude 在自己开头做了同样的事;detector 路径之前漏了这一步,导致
|
|
685
|
+
// resume / mode-switch(spawnSession skipTelegram=true)后第一条权限弹窗被吞。
|
|
686
|
+
if (openclaw?.hasExplicitRoute && !openclaw.hasExplicitRoute(sessionId)) {
|
|
687
|
+
restorePersistedRoute(sessionId, sess.todoId)
|
|
688
|
+
}
|
|
689
|
+
|
|
661
690
|
// 1) 翻状态。markPendingConfirm 默认只接受 running → pending_confirm,但 PTY-detector
|
|
662
691
|
// 要求 anchor + ≥2 数字选项才 emit,假阳性概率极低;显式 allowIdleFlip=true 让它
|
|
663
692
|
// 在 status=idle 时也能翻(覆盖 auto 模式权限框在 Stop hook 后才浮出的场景)。
|
|
@@ -669,8 +698,9 @@ export function createOpenClawHookHandler(deps = {}) {
|
|
|
669
698
|
})
|
|
670
699
|
} catch { /* ignore */ }
|
|
671
700
|
|
|
672
|
-
// 2) IM 推送资格 & cooldown 跟真 Notification 共享 →
|
|
673
|
-
|
|
701
|
+
// 2) IM 推送资格 & cooldown 跟真 Notification 共享 → 不会双推。
|
|
702
|
+
// detector 路径用 isDetectorPushEligible(不查 bypass mode),原因见函数注释。
|
|
703
|
+
if (!isDetectorPushEligible(sessionId)) {
|
|
674
704
|
return { ok: true, action: 'skipped', reason: 'im_push_not_eligible' }
|
|
675
705
|
}
|
|
676
706
|
const cd = notificationCooldownMs()
|