@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.
- package/AGENTS.md +103 -0
- package/CLAUDE.md +9 -0
- package/Makefile +120 -0
- package/README.md +1 -1
- package/avaudioengine_player_darwin.go +315 -0
- package/configs/hook-speak.sh +45 -6
- package/docs/hook-text-extraction.md +60 -27
- package/main_test.go +751 -0
- package/package.json +7 -1
- package/scripts/ispeak +1 -1
- package/stream_player_unsupported.go +9 -0
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
推荐优先级:
|
|
8
8
|
|
|
9
|
-
1. **Codex `notify`**:从脚本第二个参数 `$2` 读取 JSON,取 `last-assistant-message
|
|
10
|
-
2. **
|
|
11
|
-
3.
|
|
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
|
|
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
|
|
117
|
-
- `codex-rs/hooks/
|
|
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
|
|
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
|
-
"
|
|
184
|
+
"effort": {
|
|
185
|
+
"level": "medium"
|
|
186
|
+
}
|
|
164
187
|
}
|
|
165
188
|
```
|
|
166
189
|
|
|
167
|
-
|
|
190
|
+
子 agent 上下文中额外字段:
|
|
168
191
|
|
|
169
192
|
```json
|
|
170
193
|
{
|
|
171
|
-
"
|
|
194
|
+
"agent_id": "subagent_xyz",
|
|
195
|
+
"agent_type": "Explore"
|
|
172
196
|
}
|
|
173
197
|
```
|
|
174
198
|
|
|
175
|
-
|
|
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.
|
|
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 路径上补一个短轮询。
|