agentquad 0.3.0

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 (163) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +318 -0
  3. package/dist-web/assets/index-CMaXwixo.js +1234 -0
  4. package/dist-web/assets/index-DBHApzV1.css +32 -0
  5. package/dist-web/assets/inter-cyrillic-400-normal-HOLc17fK.woff +0 -0
  6. package/dist-web/assets/inter-cyrillic-400-normal-obahsSVq.woff2 +0 -0
  7. package/dist-web/assets/inter-cyrillic-500-normal-BasfLYem.woff2 +0 -0
  8. package/dist-web/assets/inter-cyrillic-500-normal-CxZf_p3X.woff +0 -0
  9. package/dist-web/assets/inter-cyrillic-600-normal-4D_pXhcN.woff +0 -0
  10. package/dist-web/assets/inter-cyrillic-600-normal-CWCymEST.woff2 +0 -0
  11. package/dist-web/assets/inter-cyrillic-700-normal-CjBOestx.woff2 +0 -0
  12. package/dist-web/assets/inter-cyrillic-700-normal-DrXBdSj3.woff +0 -0
  13. package/dist-web/assets/inter-cyrillic-ext-400-normal-BQZuk6qB.woff2 +0 -0
  14. package/dist-web/assets/inter-cyrillic-ext-400-normal-DQukG94-.woff +0 -0
  15. package/dist-web/assets/inter-cyrillic-ext-500-normal-B0yAr1jD.woff2 +0 -0
  16. package/dist-web/assets/inter-cyrillic-ext-500-normal-BmqWE9Dz.woff +0 -0
  17. package/dist-web/assets/inter-cyrillic-ext-600-normal-Bcila6Z-.woff +0 -0
  18. package/dist-web/assets/inter-cyrillic-ext-600-normal-Dfes3d0z.woff2 +0 -0
  19. package/dist-web/assets/inter-cyrillic-ext-700-normal-BjwYoWNd.woff2 +0 -0
  20. package/dist-web/assets/inter-cyrillic-ext-700-normal-LO58E6JB.woff +0 -0
  21. package/dist-web/assets/inter-greek-400-normal-B4URO6DV.woff2 +0 -0
  22. package/dist-web/assets/inter-greek-400-normal-q2sYcFCs.woff +0 -0
  23. package/dist-web/assets/inter-greek-500-normal-BIZE56-Y.woff2 +0 -0
  24. package/dist-web/assets/inter-greek-500-normal-Xzm54t5V.woff +0 -0
  25. package/dist-web/assets/inter-greek-600-normal-BZpKdvQh.woff +0 -0
  26. package/dist-web/assets/inter-greek-600-normal-plRanbMR.woff2 +0 -0
  27. package/dist-web/assets/inter-greek-700-normal-BUv2fZ6O.woff +0 -0
  28. package/dist-web/assets/inter-greek-700-normal-C3JjAnD8.woff2 +0 -0
  29. package/dist-web/assets/inter-greek-ext-400-normal-DGGRlc-M.woff2 +0 -0
  30. package/dist-web/assets/inter-greek-ext-400-normal-KugGGMne.woff +0 -0
  31. package/dist-web/assets/inter-greek-ext-500-normal-2j5mBUwD.woff +0 -0
  32. package/dist-web/assets/inter-greek-ext-500-normal-C4iEst2y.woff2 +0 -0
  33. package/dist-web/assets/inter-greek-ext-600-normal-B8X0CLgF.woff +0 -0
  34. package/dist-web/assets/inter-greek-ext-600-normal-DRtmH8MT.woff2 +0 -0
  35. package/dist-web/assets/inter-greek-ext-700-normal-BoQ6DsYi.woff +0 -0
  36. package/dist-web/assets/inter-greek-ext-700-normal-qfdV9bQt.woff2 +0 -0
  37. package/dist-web/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
  38. package/dist-web/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
  39. package/dist-web/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
  40. package/dist-web/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
  41. package/dist-web/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
  42. package/dist-web/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
  43. package/dist-web/assets/inter-latin-700-normal-BLAVimhd.woff +0 -0
  44. package/dist-web/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
  45. package/dist-web/assets/inter-latin-ext-400-normal-77YHD8bZ.woff +0 -0
  46. package/dist-web/assets/inter-latin-ext-400-normal-C1nco2VV.woff2 +0 -0
  47. package/dist-web/assets/inter-latin-ext-500-normal-BxGbmqWO.woff +0 -0
  48. package/dist-web/assets/inter-latin-ext-500-normal-CV4jyFjo.woff2 +0 -0
  49. package/dist-web/assets/inter-latin-ext-600-normal-CIVaiw4L.woff +0 -0
  50. package/dist-web/assets/inter-latin-ext-600-normal-D2bJ5OIk.woff2 +0 -0
  51. package/dist-web/assets/inter-latin-ext-700-normal-Ca8adRJv.woff2 +0 -0
  52. package/dist-web/assets/inter-latin-ext-700-normal-TidjK2hL.woff +0 -0
  53. package/dist-web/assets/inter-vietnamese-400-normal-Bbgyi5SW.woff +0 -0
  54. package/dist-web/assets/inter-vietnamese-400-normal-DMkecbls.woff2 +0 -0
  55. package/dist-web/assets/inter-vietnamese-500-normal-DOriooB6.woff2 +0 -0
  56. package/dist-web/assets/inter-vietnamese-500-normal-mJboJaSs.woff +0 -0
  57. package/dist-web/assets/inter-vietnamese-600-normal-BuLX-rYi.woff +0 -0
  58. package/dist-web/assets/inter-vietnamese-600-normal-Cc8MFFhd.woff2 +0 -0
  59. package/dist-web/assets/inter-vietnamese-700-normal-BZaoP0fm.woff +0 -0
  60. package/dist-web/assets/inter-vietnamese-700-normal-DlLaEgI2.woff2 +0 -0
  61. package/dist-web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  62. package/dist-web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  63. package/dist-web/assets/jetbrains-mono-cyrillic-700-normal-BWTpRfYl.woff2 +0 -0
  64. package/dist-web/assets/jetbrains-mono-cyrillic-700-normal-CEoEElIJ.woff +0 -0
  65. package/dist-web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  66. package/dist-web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  67. package/dist-web/assets/jetbrains-mono-greek-700-normal-C6CZE3T8.woff2 +0 -0
  68. package/dist-web/assets/jetbrains-mono-greek-700-normal-DEigVDxa.woff +0 -0
  69. package/dist-web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  70. package/dist-web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  71. package/dist-web/assets/jetbrains-mono-latin-700-normal-BYuf6tUa.woff2 +0 -0
  72. package/dist-web/assets/jetbrains-mono-latin-700-normal-D3wTyLJW.woff +0 -0
  73. package/dist-web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  74. package/dist-web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  75. package/dist-web/assets/jetbrains-mono-latin-ext-700-normal-CZipNAKV.woff2 +0 -0
  76. package/dist-web/assets/jetbrains-mono-latin-ext-700-normal-CxPITLHs.woff +0 -0
  77. package/dist-web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  78. package/dist-web/assets/jetbrains-mono-vietnamese-700-normal-BDLVIk2r.woff +0 -0
  79. package/dist-web/assets/logo-D4DDtU-r.png +0 -0
  80. package/dist-web/favicon.png +0 -0
  81. package/dist-web/index.html +14 -0
  82. package/package.json +88 -0
  83. package/src/ask-user-buttons.js +142 -0
  84. package/src/claude-transcript.js +203 -0
  85. package/src/cli.js +1040 -0
  86. package/src/codex-event-emitter.js +111 -0
  87. package/src/codex-prompt-detector.js +53 -0
  88. package/src/codex-sidecar.js +52 -0
  89. package/src/codex-transcript.js +74 -0
  90. package/src/config.js +692 -0
  91. package/src/data/claude-code-commands.json +52 -0
  92. package/src/db.js +1503 -0
  93. package/src/dispatch.js +13 -0
  94. package/src/export/todoMarkdown.js +246 -0
  95. package/src/first-run-wizard.js +82 -0
  96. package/src/git/gitStatus.js +139 -0
  97. package/src/lark-api-client.js +205 -0
  98. package/src/lark-bot.js +510 -0
  99. package/src/lark-card.js +88 -0
  100. package/src/lark-config-service.js +16 -0
  101. package/src/lark-event-client.js +107 -0
  102. package/src/lark-image.js +99 -0
  103. package/src/lark-markdown.js +51 -0
  104. package/src/lark-video.js +163 -0
  105. package/src/mcp/audit.js +34 -0
  106. package/src/mcp/server.js +83 -0
  107. package/src/mcp/tools/destructive/index.js +252 -0
  108. package/src/mcp/tools/openclaw/index.js +405 -0
  109. package/src/mcp/tools/read/index.js +269 -0
  110. package/src/mcp/tools/write/index.js +157 -0
  111. package/src/openclaw-bridge.js +566 -0
  112. package/src/openclaw-hook-installer.js +338 -0
  113. package/src/openclaw-hook.js +908 -0
  114. package/src/openclaw-wizard.js +2442 -0
  115. package/src/pending-questions.js +297 -0
  116. package/src/pricing.js +45 -0
  117. package/src/prompt-render.js +36 -0
  118. package/src/pty.js +992 -0
  119. package/src/routes/ai-terminal.js +1228 -0
  120. package/src/routes/git.js +89 -0
  121. package/src/routes/openclaw-hook.js +67 -0
  122. package/src/routes/openclaw-inbound.js +36 -0
  123. package/src/routes/recurringRules.js +80 -0
  124. package/src/routes/reports.js +50 -0
  125. package/src/routes/search.js +46 -0
  126. package/src/routes/stats.js +31 -0
  127. package/src/routes/telegram-config.js +152 -0
  128. package/src/routes/telegram-sync.js +221 -0
  129. package/src/routes/templates.js +63 -0
  130. package/src/routes/todos.js +649 -0
  131. package/src/routes/transcripts.js +75 -0
  132. package/src/routes/uploads.js +107 -0
  133. package/src/routes/wiki.js +142 -0
  134. package/src/search/fts.js +209 -0
  135. package/src/search/index.js +199 -0
  136. package/src/search/transcripts.js +148 -0
  137. package/src/server.js +1791 -0
  138. package/src/session-input-dispatcher.js +256 -0
  139. package/src/stats/markdown.js +42 -0
  140. package/src/stats/report.js +207 -0
  141. package/src/summarize.js +84 -0
  142. package/src/system-rules.js +52 -0
  143. package/src/telegram-bot.js +875 -0
  144. package/src/telegram-commands.js +149 -0
  145. package/src/telegram-config-service.js +84 -0
  146. package/src/telegram-image.js +95 -0
  147. package/src/telegram-loading-status.js +112 -0
  148. package/src/telegram-markdown.js +82 -0
  149. package/src/telegram-reaction-tracker.js +69 -0
  150. package/src/telegram-video.js +75 -0
  151. package/src/templates/claude-hooks/notify.js +103 -0
  152. package/src/transcript.js +305 -0
  153. package/src/transcripts/blocks.js +56 -0
  154. package/src/transcripts/index.js +222 -0
  155. package/src/transcripts/indexer.js +34 -0
  156. package/src/transcripts/matcher.js +70 -0
  157. package/src/transcripts/scanner.js +259 -0
  158. package/src/usage-footer.js +170 -0
  159. package/src/usage-parser.js +132 -0
  160. package/src/wiki/guide.js +44 -0
  161. package/src/wiki/index.js +232 -0
  162. package/src/wiki/redact.js +34 -0
  163. package/src/wiki/sources.js +122 -0
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Pending-question 协调器:
3
+ * - ticket 生成(3 字符 base32, RFC 4648 字符集 a-z2-7,转小写)
4
+ * - DB 持久化(pending_questions 表)
5
+ * - Promise 池(ticket → resolveFn)让 ask_user MCP 工具能阻塞等待用户回复
6
+ * - 用户回复路由:解析 ticket prefix → fallback 最近一条 → 选项模糊匹配
7
+ * - 超时 sweeper:定期把过期 pending 标 timeout 并 reject 对应 promise
8
+ */
9
+ import { randomInt } from 'node:crypto'
10
+
11
+ const TICKET_ALPHABET = 'abcdefghijklmnopqrstuvwxyz234567' // 32 字符
12
+ const TICKET_LENGTH = 3
13
+ const MAX_GENERATE_RETRIES = 16
14
+
15
+ const DEFAULT_SWEEP_INTERVAL_MS = 30_000
16
+
17
+ function generateTicket() {
18
+ let out = ''
19
+ for (let i = 0; i < TICKET_LENGTH; i++) {
20
+ out += TICKET_ALPHABET[randomInt(0, TICKET_ALPHABET.length)]
21
+ }
22
+ return out
23
+ }
24
+
25
+ const EXPLICIT_TICKET_RE = new RegExp(`^#([${TICKET_ALPHABET}]{${TICKET_LENGTH}})\\b`, 'i')
26
+ const BARE_TICKET_RE = new RegExp(`^([${TICKET_ALPHABET}]{${TICKET_LENGTH}})\\b`, 'i')
27
+
28
+ function stripPrefixSeparators(s) {
29
+ return s.replace(/^[\s,:#]+/, '').trim()
30
+ }
31
+
32
+ /**
33
+ * 纯选项匹配。不再尝试提取 ticket(ticket 提取改由 submitReply 配合 DB 判断)。
34
+ *
35
+ * 优先级:
36
+ * 1) 整段 startswith 数字 1..N → options[index-1]
37
+ * 2) startswith / contains 任一 option(大小写不敏感)
38
+ * 3) 都不匹配 → free text,原文返回
39
+ */
40
+ export function parseReply(rawText, options = []) {
41
+ const text = String(rawText || '').trim()
42
+ if (!text) return { freeText: '', raw: rawText, chosenIndex: null }
43
+
44
+ // 1) 纯数字 1..N
45
+ const numMatch = text.match(/^(\d+)\b/)
46
+ if (numMatch) {
47
+ const idx = parseInt(numMatch[1], 10) - 1
48
+ if (idx >= 0 && idx < options.length) {
49
+ return { chosenIndex: idx, freeText: text, raw: rawText }
50
+ }
51
+ }
52
+
53
+ // 2) 选项文本匹配 — 先严格 startswith / 再宽松 contains
54
+ const lower = text.toLowerCase()
55
+ for (let i = 0; i < options.length; i++) {
56
+ const opt = String(options[i] || '').toLowerCase()
57
+ if (!opt) continue
58
+ if (lower === opt || lower.startsWith(opt) || opt.startsWith(lower)) {
59
+ return { chosenIndex: i, freeText: text, raw: rawText }
60
+ }
61
+ }
62
+ for (let i = 0; i < options.length; i++) {
63
+ const opt = String(options[i] || '').toLowerCase()
64
+ if (opt && lower.includes(opt)) {
65
+ return { chosenIndex: i, freeText: text, raw: rawText }
66
+ }
67
+ }
68
+
69
+ return { chosenIndex: null, freeText: text, raw: rawText }
70
+ }
71
+
72
+ /**
73
+ * 从用户回复中尝试提取 ticket 候选。
74
+ * - `#xxx ...` 显式 → 强制路由(即使 ticket 不存在也按 ticket 处理,让上层回 ticket_not_pending)
75
+ * - `xxx ...` 裸前缀 → 软路由(仅当上层确认 ticket 在 DB 里 pending 才采用)
76
+ *
77
+ * 返回 { explicit, candidate, body }。
78
+ */
79
+ export function extractTicketCandidate(rawText) {
80
+ const text = String(rawText || '').trim()
81
+ const exp = text.match(EXPLICIT_TICKET_RE)
82
+ if (exp) {
83
+ return {
84
+ explicit: exp[1].toLowerCase(),
85
+ candidate: null,
86
+ body: stripPrefixSeparators(text.slice(exp[0].length)),
87
+ }
88
+ }
89
+ const bare = text.match(BARE_TICKET_RE)
90
+ if (bare) {
91
+ return {
92
+ explicit: null,
93
+ candidate: bare[1].toLowerCase(),
94
+ body: stripPrefixSeparators(text.slice(bare[0].length)),
95
+ }
96
+ }
97
+ return { explicit: null, candidate: null, body: text }
98
+ }
99
+
100
+ /**
101
+ * 启动一个协调器实例。
102
+ *
103
+ * 依赖:
104
+ * - db: 暴露 createPendingQuestion / getPendingQuestion / answerPendingQuestion /
105
+ * setPendingStatus / getLatestPendingQuestion / sweepExpiredPendingQuestions
106
+ */
107
+ export function createPendingQuestionCoordinator({ db, sweepIntervalMs = DEFAULT_SWEEP_INTERVAL_MS, logger = console } = {}) {
108
+ if (!db) throw new Error('db_required')
109
+
110
+ // ticket → { resolve, reject, options, createdAt, expiresAt }
111
+ const waiters = new Map()
112
+
113
+ function makeFreshTicket() {
114
+ for (let i = 0; i < MAX_GENERATE_RETRIES; i++) {
115
+ const t = generateTicket()
116
+ const existing = db.getPendingQuestion(t)
117
+ if (!existing || existing.status !== 'pending') return t
118
+ }
119
+ throw new Error('ticket_generate_exhausted')
120
+ }
121
+
122
+ /**
123
+ * 创建一条 pending question 并返回一个会 resolve 成 result 的 Promise。
124
+ * 调用方(ask_user MCP)应 await 这个 Promise;
125
+ * 若超时或被取消,会 resolve 成 { status: 'timeout' | 'cancelled' }(不 reject)。
126
+ */
127
+ function ask({ sessionId, todoId, question, options, timeoutMs = 600_000 }) {
128
+ if (!sessionId) throw new Error('session_id_required')
129
+ if (!question) throw new Error('question_required')
130
+ if (!Array.isArray(options) || options.length === 0) throw new Error('options_required')
131
+
132
+ const ticket = makeFreshTicket()
133
+ db.createPendingQuestion({ ticket, sessionId, todoId, question, options, timeoutMs })
134
+
135
+ const promise = new Promise((resolve) => {
136
+ const startedAt = Date.now()
137
+ const timer = setTimeout(() => {
138
+ const w = waiters.get(ticket)
139
+ if (!w) return
140
+ waiters.delete(ticket)
141
+ try { db.setPendingStatus(ticket, 'timeout') } catch {}
142
+ resolve({
143
+ ticket,
144
+ status: 'timeout',
145
+ chosen: null,
146
+ chosenIndex: null,
147
+ answerText: null,
148
+ elapsedMs: Date.now() - startedAt,
149
+ })
150
+ }, timeoutMs)
151
+ timer.unref?.()
152
+ waiters.set(ticket, {
153
+ resolve: (payload) => {
154
+ clearTimeout(timer)
155
+ waiters.delete(ticket)
156
+ resolve({ ticket, ...payload, elapsedMs: Date.now() - startedAt })
157
+ },
158
+ options,
159
+ startedAt,
160
+ })
161
+ })
162
+
163
+ return { ticket, promise }
164
+ }
165
+
166
+ /**
167
+ * 根据用户回复路由到一条 pending question。
168
+ * 优先级:ticket prefix → 最近一条 pending。
169
+ * 返回 { matched: true, ticket, chosen, chosenIndex, answerText } 或
170
+ * { matched: false, reason }
171
+ */
172
+ function submitReply(rawText) {
173
+ const text = String(rawText || '').trim()
174
+ if (!text) return { matched: false, reason: 'empty' }
175
+
176
+ const { explicit, candidate, body: bodyAfterPrefix } = extractTicketCandidate(text)
177
+ let target = null
178
+ let matchBody = text
179
+
180
+ if (explicit) {
181
+ target = db.getPendingQuestion(explicit)
182
+ if (!target || target.status !== 'pending') {
183
+ return { matched: false, reason: 'ticket_not_pending', ticket: explicit }
184
+ }
185
+ matchBody = bodyAfterPrefix
186
+ } else if (candidate) {
187
+ const probe = db.getPendingQuestion(candidate)
188
+ if (probe && probe.status === 'pending') {
189
+ target = probe
190
+ matchBody = bodyAfterPrefix
191
+ }
192
+ }
193
+ if (!target) {
194
+ target = db.getLatestPendingQuestion()
195
+ if (!target) return { matched: false, reason: 'no_pending' }
196
+ // bare-prefix 没命中真 ticket:用全文做选项匹配(不剥前缀)
197
+ matchBody = text
198
+ }
199
+
200
+ const parsed = parseReply(matchBody, target.options)
201
+ const answerText = parsed.freeText || matchBody || text
202
+
203
+ db.answerPendingQuestion(target.ticket, {
204
+ answerText,
205
+ chosenIndex: parsed.chosenIndex,
206
+ })
207
+
208
+ const waiter = waiters.get(target.ticket)
209
+ const chosen = parsed.chosenIndex != null
210
+ ? target.options[parsed.chosenIndex]
211
+ : null
212
+ if (waiter) {
213
+ waiter.resolve({
214
+ status: 'answered',
215
+ chosen,
216
+ chosenIndex: parsed.chosenIndex,
217
+ answerText,
218
+ })
219
+ }
220
+
221
+ return {
222
+ matched: true,
223
+ ticket: target.ticket,
224
+ todoId: target.todoId,
225
+ sessionId: target.sessionId,
226
+ chosen,
227
+ chosenIndex: parsed.chosenIndex,
228
+ answerText,
229
+ }
230
+ }
231
+
232
+ function cancel(ticket, reason = 'user_cancelled') {
233
+ const existing = db.getPendingQuestion(ticket)
234
+ if (!existing) return { ok: false, reason: 'not_found' }
235
+ if (existing.status !== 'pending') return { ok: false, reason: 'not_pending', status: existing.status }
236
+ db.setPendingStatus(ticket, 'cancelled')
237
+ const waiter = waiters.get(ticket)
238
+ if (waiter) {
239
+ waiter.resolve({
240
+ status: 'cancelled',
241
+ chosen: null,
242
+ chosenIndex: null,
243
+ answerText: reason || null,
244
+ })
245
+ }
246
+ return { ok: true, ticket }
247
+ }
248
+
249
+ function listPending() {
250
+ return db.listPendingQuestions().map((row) => ({
251
+ ...row,
252
+ ageSeconds: Math.floor((Date.now() - row.createdAt) / 1000),
253
+ remainingSeconds: Math.max(0, Math.floor(((row.createdAt + row.timeoutMs) - Date.now()) / 1000)),
254
+ }))
255
+ }
256
+
257
+ function sweep() {
258
+ try {
259
+ const changed = db.sweepExpiredPendingQuestions()
260
+ if (changed > 0) logger.info?.(`[pending-questions] swept ${changed} expired`)
261
+ } catch (e) {
262
+ logger.warn?.(`[pending-questions] sweep failed: ${e.message}`)
263
+ }
264
+ }
265
+
266
+ // 启动后台 sweeper
267
+ let sweepTimer = null
268
+ function start() {
269
+ if (sweepTimer) return
270
+ sweepTimer = setInterval(sweep, sweepIntervalMs)
271
+ sweepTimer.unref?.()
272
+ }
273
+ function stop() {
274
+ if (sweepTimer) { clearInterval(sweepTimer); sweepTimer = null }
275
+ }
276
+
277
+ return {
278
+ ask,
279
+ submitReply,
280
+ cancel,
281
+ listPending,
282
+ sweep,
283
+ start,
284
+ stop,
285
+ // 测试用
286
+ _waiters: waiters,
287
+ _generateTicket: generateTicket,
288
+ }
289
+ }
290
+
291
+ export const __test__ = {
292
+ generateTicket,
293
+ parseReply,
294
+ extractTicketCandidate,
295
+ TICKET_ALPHABET,
296
+ TICKET_LENGTH,
297
+ }
package/src/pricing.js ADDED
@@ -0,0 +1,45 @@
1
+ export const DEFAULT_PRICING = {
2
+ default: { input: 3.00, output: 15.00, cacheRead: 0.30, cacheWrite: 3.75 },
3
+ models: {
4
+ // 注意 wildcard 紧贴 4,没有 -:这样 normalize 过的 'claude-opus-4'(无日期后缀)
5
+ // 和原始 'claude-opus-4-20260101' 都能匹配。usage-parser.normalizeModel 会剥日期,
6
+ // 用 '-*' 时空后缀不匹配 → opus / haiku 静默回落到 default 价(sonnet 巧合相同
7
+ // 所以早期没暴露),opus 实际价 5x default → 被低估。
8
+ 'claude-opus-4*': { input: 15.00, output: 75.00, cacheRead: 1.50, cacheWrite: 18.75 },
9
+ 'claude-sonnet-4*': { input: 3.00, output: 15.00, cacheRead: 0.30, cacheWrite: 3.75 },
10
+ 'claude-haiku-4*': { input: 1.00, output: 5.00, cacheRead: 0.10, cacheWrite: 1.25 },
11
+ // OpenAI/GPT — USD per 1M tokens. cacheWrite 沿用 cacheRead(OpenAI 只暴露
12
+ // cached input 一档,没有 Anthropic 式的 cache creation)。'gpt-4o-mini*'
13
+ // 必须排在 'gpt-4o*' 之前,否则 mini 会先命中 4o 主价表。
14
+ 'gpt-5*': { input: 1.25, output: 10.00, cacheRead: 0.125, cacheWrite: 0.125 },
15
+ 'gpt-4.1*': { input: 2.00, output: 8.00, cacheRead: 0.50, cacheWrite: 0.50 },
16
+ 'gpt-4o-mini*': { input: 0.15, output: 0.60, cacheRead: 0.075, cacheWrite: 0.075 },
17
+ 'gpt-4o*': { input: 2.50, output: 10.00, cacheRead: 1.25, cacheWrite: 1.25 },
18
+ },
19
+ cnyRate: 7.2,
20
+ }
21
+
22
+ function globToRegex(glob) {
23
+ const escaped = glob.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*')
24
+ return new RegExp(`^${escaped}$`)
25
+ }
26
+
27
+ function resolveRate(model, pricing) {
28
+ if (model && pricing.models) {
29
+ for (const [pattern, rate] of Object.entries(pricing.models)) {
30
+ if (globToRegex(pattern).test(model)) return rate
31
+ }
32
+ }
33
+ return pricing.default
34
+ }
35
+
36
+ export function estimateCost(tokens, model, pricing = DEFAULT_PRICING) {
37
+ const rate = resolveRate(model, pricing)
38
+ const usd =
39
+ (Number(tokens.input) || 0) * rate.input / 1_000_000 +
40
+ (Number(tokens.output) || 0) * rate.output / 1_000_000 +
41
+ (Number(tokens.cacheRead) || 0) * rate.cacheRead / 1_000_000 +
42
+ (Number(tokens.cacheCreation) || 0) * rate.cacheWrite / 1_000_000
43
+ const cnyRate = pricing.cnyRate ?? DEFAULT_PRICING.cnyRate
44
+ return { usd, cny: usd * cnyRate }
45
+ }
@@ -0,0 +1,36 @@
1
+ const QUADRANT_LABEL = {
2
+ 1: '重要且紧急',
3
+ 2: '重要不紧急',
4
+ 3: '紧急不重要',
5
+ 4: '不重要不紧急',
6
+ }
7
+
8
+ export function buildVars(todo) {
9
+ if (!todo) return {}
10
+ const dueDate = todo.dueDate
11
+ ? new Date(todo.dueDate).toISOString().slice(0, 10)
12
+ : ''
13
+ return {
14
+ title: todo.title || '',
15
+ description: todo.description || '',
16
+ workDir: todo.workDir || '',
17
+ quadrant: todo.quadrant ? `Q${todo.quadrant}(${QUADRANT_LABEL[todo.quadrant] || ''})` : '',
18
+ dueDate,
19
+ }
20
+ }
21
+
22
+ export function renderTemplate(content, vars) {
23
+ if (!content) return ''
24
+ return String(content).replace(/\{\{\s*(\w+)\s*\}\}/g, (_m, key) => {
25
+ const v = vars?.[key]
26
+ return v == null ? '' : String(v)
27
+ })
28
+ }
29
+
30
+ export function renderTemplates(templates, vars, { separator = '\n\n---\n\n' } = {}) {
31
+ if (!Array.isArray(templates) || templates.length === 0) return ''
32
+ return templates
33
+ .map(t => renderTemplate(typeof t === 'string' ? t : t?.content || '', vars).trim())
34
+ .filter(Boolean)
35
+ .join(separator)
36
+ }