@xdfnet/ispeak 1.6.8 → 1.6.10

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.
@@ -6,9 +6,9 @@
6
6
 
7
7
  推荐优先级:
8
8
 
9
- 1. **Codex `notify`**:从脚本第二个参数 `$2` 读取 JSON,取 `last-assistant-message`。
10
- 2. **Claude / Codex Stop Hook**:从 stdin 读取 JSON,优先取 `last_assistant_message`。
11
- 3. **明确 transcript**:如果没有直接字段,只读取 payload 里明确传入的 `transcript_path`。
9
+ 1. **Codex `notify`**:从脚本第二个参数 `$2` 读取 JSON,取 `last-assistant-message`(kebab-case)。
10
+ 2. **Codex Stop Hook**:从 stdin 读取 JSON,取 `last_assistant_message`(snake_case)。
11
+ 3. **Claude Code Stop Hook**:从 stdin 读取 JSON,只读 `transcript_path`(官方无 direct 字段)。
12
12
 
13
13
  不扫描 `~/.codex/sessions`。没有 direct 字段也没有 `transcript_path` 时,本次不播报。
14
14
 
@@ -61,7 +61,7 @@ input="${2:-}"
61
61
  payload["last-assistant-message"]
62
62
  ```
63
63
 
64
- 源码依据:`codex-rs/hooks/src/legacy_notify.rs`。该文件把 `last_assistant_message` 序列化为 kebab-case 的 `last-assistant-message`,并在执行命令前 `command.arg(notify_payload)`。
64
+ 源码依据:`codex-rs/hooks/src/legacy_notify.rs`(https://github.com/openai/codex,2026-05-11)。该文件把 `last_assistant_message` 序列化为 kebab-case 的 `last-assistant-message`,并在执行命令前 `command.arg(notify_payload)`。
65
65
 
66
66
  ## Codex CLI:Stop Hook
67
67
 
@@ -95,7 +95,23 @@ $2 = empty
95
95
  stdin = '{"hook_event_name":"Stop",...,"last_assistant_message":"..."}'
96
96
  ```
97
97
 
98
- 核心字段:
98
+ 核心字段(源码 `StopCommandInput` struct):
99
+
100
+ ```rust
101
+ struct StopCommandInput {
102
+ session_id: String,
103
+ turn_id: String,
104
+ transcript_path: NullableString,
105
+ cwd: String,
106
+ hook_event_name: String,
107
+ model: String,
108
+ permission_mode: String,
109
+ stop_hook_active: bool,
110
+ last_assistant_message: NullableString, // ← Codex 有此字段
111
+ }
112
+ ```
113
+
114
+ 对应 JSON:
99
115
 
100
116
  ```json
101
117
  {
@@ -113,9 +129,8 @@ stdin = '{"hook_event_name":"Stop",...,"last_assistant_message":"..."}'
113
129
 
114
130
  源码依据:
115
131
 
116
- - `codex-rs/hooks/src/events/stop.rs`:构造 `StopCommandInput`,包含 `last_assistant_message` 和 `transcript_path`。
117
- - `codex-rs/hooks/schema/generated/stop.command.input.schema.json`:Stop stdin schema。
118
- - `codex-rs/hooks/src/engine/command_runner.rs`:Hook 命令通过 stdin 接收 `input_json`。
132
+ - `codex-rs/hooks/src/events/stop.rs`(https://github.com/openai/codex,2026-05-11):构造 `StopCommandInput`,包含 `last_assistant_message` 和 `transcript_path`。
133
+ - `codex-rs/hooks/src/engine/command_runner.rs`(同上):Hook 命令通过 stdin 接收 `input_json`。
119
134
 
120
135
  ## Codex Transcript
121
136
 
@@ -153,30 +168,35 @@ event.payload.content[].text
153
168
 
154
169
  ## Claude Code:Stop Hook
155
170
 
156
- Claude Code 官方 Stop Hook 通过 stdin 传 JSON,核心字段是:
171
+ > **来源**:[Claude Code Hooks Reference](https://code.claude.com/docs/en/hooks.md),更新时间:2026-05-11
172
+
173
+ Claude Code 官方 Stop Hook **没有 `last_assistant_message` 字段**。
174
+
175
+ 根据官方文档,Stop Hook 的 Common Input Fields 为:
157
176
 
158
177
  ```json
159
178
  {
160
- "session_id": "...",
161
- "transcript_path": "...",
179
+ "session_id": "abc123",
180
+ "transcript_path": "/home/user/.claude/projects/.../transcript.jsonl",
181
+ "cwd": "/home/user/my-project",
182
+ "permission_mode": "default",
162
183
  "hook_event_name": "Stop",
163
- "stop_hook_active": false
184
+ "effort": {
185
+ "level": "medium"
186
+ }
164
187
  }
165
188
  ```
166
189
 
167
- 有些版本或场景可能直接提供:
190
+ 子 agent 上下文中额外字段:
168
191
 
169
192
  ```json
170
193
  {
171
- "last_assistant_message": "最后一条 assistant 回复"
194
+ "agent_id": "subagent_xyz",
195
+ "agent_type": "Explore"
172
196
  }
173
197
  ```
174
198
 
175
- 所以 Claude 的读取顺序是:
176
-
177
- 1. `last_assistant_message`
178
- 2. `message`
179
- 3. `transcript_path`
199
+ **结论**:Claude Code Stop Hook 官方设计只提供 `transcript_path`,没有直接内嵌 `last_assistant_message`。旧版本脚本的 `last_assistant_message` / `message` fallback 实际上**从未被官方文档支持**。
180
200
 
181
201
  Claude transcript 常见 assistant 形态:
182
202
 
@@ -207,11 +227,11 @@ fi
207
227
  - Claude / Codex Stop Hook:读 stdin
208
228
  - 如果 Codex 的 `notify` 和 `Stop` 同时启用,脚本会按 `turn_id` 去重,避免同一回合播两次
209
229
 
210
- Codex 文本字段优先级:
230
+ Codex 文本字段优先级(源码确认):
211
231
 
212
232
  ```js
213
- payload["last-assistant-message"]
214
- payload.last_assistant_message
233
+ payload["last-assistant-message"] // notify: kebab-case
234
+ payload.last_assistant_message // Stop Hook: snake_case
215
235
  payload.lastAssistantMessage
216
236
  payload.message
217
237
  payload.lastMessage
@@ -220,15 +240,14 @@ payload.transcriptPath
220
240
  payload["transcript-path"]
221
241
  ```
222
242
 
223
- Claude 文本字段优先级:
243
+ Claude Code 文本字段优先级(官方文档):
224
244
 
225
245
  ```js
226
- payload.last_assistant_message
227
- payload.message
228
- payload.transcript_path
229
- payload.transcriptPath
246
+ payload.transcript_path // 官方支持的唯一方式
230
247
  ```
231
248
 
249
+ > **注**:Claude Code Stop Hook 官方 payload 中**没有 `last_assistant_message` 字段**,这是与 Codex 的本质区别。
250
+
232
251
  ## 为什么不能只读 stdin
233
252
 
234
253
  因为 Codex `notify` 不走 stdin。只读 stdin 会导致:
@@ -239,3 +258,17 @@ SPOKE: SKIP
239
258
  ```
240
259
 
241
260
  正确做法是先读 `$2`,再读 stdin;不扫历史 session。
261
+
262
+ ## Claude Code TEXT_LEN: 0 的根因
263
+
264
+ 当 Claude Code Stop Hook 触发但 `TEXT_LEN: 0` 时:
265
+
266
+ 1. **官方字段不存在**:Claude Code Stop Hook 官方 payload 中**没有 `last_assistant_message` 字段**,只有 `transcript_path`
267
+ 2. **transcript 文件可能晚一点才写完**:Hook 触发时文件虽已存在,但最后一条 assistant 文本还没落盘
268
+ 3. **结果**:如果只读一次,`hook-speak.sh` 可能拿到空串,本次不播报
269
+
270
+ 当前脚本对 Claude transcript 做了很短的轮询,等最后一条 assistant 文本真正出现再播,避免这个时序窗。
271
+
272
+ 这是 **Claude Code 与 Codex 的设计差异**,非 bug。Codex CLI(无论 notify 还是 Stop Hook)都提供 `last_assistant_message`,而 Claude Code 官方只提供 `transcript_path`。
273
+
274
+ 解决方案:从 `transcript_path` 读取并解析为最终一条 assistant 回复,并在 Claude 路径上补一个短轮询。