@linxin666/dsh-pet 0.1.12 → 0.1.14
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.i18n.yaml +6 -0
- package/README.md +64 -59
- package/README.zh.md +113 -0
- package/lib/client.js +78 -53
- package/lib/client.js.map +1 -1
- package/lib/index.js +164 -25
- package/lib/invariant.js +1 -1
- package/lib/{state-C9EyycwI.js → state-P-wsVJ6O.js} +7 -6
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +3 -2
- package/lib/types/client/WhalePet.d.ts.map +1 -1
- package/lib/types/client/WhalePet.js +2 -1
- package/lib/types/service.d.ts +16 -5
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +159 -35
- package/lib/types/state.d.ts +10 -12
- package/lib/types/state.d.ts.map +1 -1
- package/lib/types/state.js +14 -10
- package/package.json +1 -11
- package/src/client/PluginSettingsCard.tsx +30 -5
- package/src/client/WhalePet.test.tsx +25 -1
- package/src/client/WhalePet.tsx +6 -0
- package/src/client/pet.module.css +9 -0
- package/src/client/settings-card.module.css +141 -127
- package/src/service.ts +187 -34
- package/src/state.test.ts +4 -1
- package/src/state.ts +17 -13
|
@@ -1,71 +1,83 @@
|
|
|
1
|
+
/* Plugin settings card chrome + staged form fields.
|
|
2
|
+
* Aligned with the official ui-settings-plugins PluginCard / fields CSS:
|
|
3
|
+
* same semantic tokens, radius, typography and states so family cards read
|
|
4
|
+
* as siblings of the built-in Shell / Agent loop / Web search cards. */
|
|
5
|
+
|
|
1
6
|
.card {
|
|
2
|
-
list-style: none;
|
|
3
7
|
border: 1px solid var(--dsw-alias-border-l2);
|
|
4
|
-
border-radius: 8px;
|
|
5
8
|
background: var(--dsw-alias-bg-layer-3);
|
|
6
|
-
|
|
9
|
+
border-radius: 12px;
|
|
10
|
+
list-style: none;
|
|
7
11
|
transition: border-color 0.16s, background 0.16s;
|
|
8
12
|
}
|
|
9
13
|
|
|
14
|
+
.card:hover {
|
|
15
|
+
border-color: var(--dsw-alias-label-dimmed);
|
|
16
|
+
}
|
|
17
|
+
|
|
10
18
|
.cardOpen {
|
|
11
19
|
background: var(--dsw-alias-bg-layer-2);
|
|
12
20
|
border-color: var(--dsw-alias-label-dimmed);
|
|
13
21
|
}
|
|
14
22
|
|
|
15
23
|
.header {
|
|
16
|
-
|
|
17
|
-
align-items: center;
|
|
18
|
-
gap: 8px;
|
|
24
|
+
appearance: none;
|
|
19
25
|
width: 100%;
|
|
20
|
-
padding: 10px 14px;
|
|
21
|
-
border: 0;
|
|
22
|
-
background: transparent;
|
|
23
|
-
cursor: pointer;
|
|
24
|
-
text-align: left;
|
|
25
26
|
font: inherit;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
color: inherit;
|
|
28
|
+
text-align: left;
|
|
29
|
+
cursor: pointer;
|
|
30
|
+
background: transparent;
|
|
31
|
+
border: 0;
|
|
32
|
+
border-radius: 12px;
|
|
33
|
+
align-items: center;
|
|
34
|
+
gap: 12px;
|
|
35
|
+
padding: 14px 16px;
|
|
36
|
+
display: flex;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
.header:focus-visible {
|
|
38
|
-
outline:
|
|
39
|
-
|
|
40
|
-
box-shadow: inset 0 0 0 2px var(--dsw-alias-button-info-fill);
|
|
40
|
+
outline: 2px solid var(--dsw-alias-brand-primary);
|
|
41
|
+
outline-offset: -2px;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
.headText {
|
|
44
|
-
display: flex;
|
|
45
45
|
flex-direction: column;
|
|
46
|
-
gap: 2px;
|
|
47
46
|
flex: 1;
|
|
47
|
+
gap: 4px;
|
|
48
48
|
min-width: 0;
|
|
49
|
+
display: flex;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
.name {
|
|
52
|
-
font-weight: 600;
|
|
53
53
|
color: var(--dsw-alias-label-primary);
|
|
54
|
+
font-size: 15px;
|
|
55
|
+
font-weight: 600;
|
|
56
|
+
line-height: 1.4;
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
.description {
|
|
57
|
-
font-size: 12px;
|
|
58
60
|
color: var(--dsw-alias-label-tertiary);
|
|
61
|
+
font-size: 13px;
|
|
62
|
+
line-height: 1.5;
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
.pending {
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
white-space: nowrap;
|
|
67
|
+
background: var(--dsw-alias-bg-module-platform);
|
|
68
|
+
color: var(--dsw-alias-label-secondary);
|
|
69
|
+
border-radius: 999px;
|
|
70
|
+
flex: none;
|
|
71
|
+
padding: 1px 8px;
|
|
72
|
+
font-size: 11px;
|
|
73
|
+
font-weight: 500;
|
|
74
|
+
line-height: 17px;
|
|
64
75
|
}
|
|
65
76
|
|
|
66
77
|
.chevron {
|
|
67
|
-
transition: transform 120ms ease;
|
|
68
78
|
color: var(--dsw-alias-label-tertiary);
|
|
79
|
+
flex: none;
|
|
80
|
+
transition: transform 0.16s;
|
|
69
81
|
}
|
|
70
82
|
|
|
71
83
|
.chevronOpen {
|
|
@@ -73,53 +85,59 @@
|
|
|
73
85
|
}
|
|
74
86
|
|
|
75
87
|
.body {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
gap: 14px;
|
|
88
|
+
border-top: 1px solid var(--dsw-alias-border-l2);
|
|
89
|
+
margin: 0 16px;
|
|
90
|
+
padding-bottom: 8px;
|
|
80
91
|
}
|
|
81
92
|
|
|
82
93
|
.readOnly {
|
|
83
|
-
margin: 0;
|
|
84
|
-
font-size: 12px;
|
|
85
94
|
color: var(--dsw-alias-label-tertiary);
|
|
95
|
+
margin: 12px 0 0;
|
|
96
|
+
font-size: 12px;
|
|
97
|
+
line-height: 1.5;
|
|
86
98
|
}
|
|
87
99
|
|
|
88
100
|
.notExposed {
|
|
89
|
-
|
|
101
|
+
color: var(--dsw-alias-state-warn-primary);
|
|
102
|
+
margin: 12px 0 0;
|
|
90
103
|
font-size: 12px;
|
|
91
104
|
line-height: 1.5;
|
|
92
|
-
color: var(--dsw-alias-state-warn-primary);
|
|
93
105
|
}
|
|
94
106
|
|
|
95
107
|
.footer {
|
|
96
|
-
|
|
97
|
-
align-items: center;
|
|
108
|
+
border-top: 1px solid var(--dsw-alias-border-l2);
|
|
98
109
|
justify-content: flex-end;
|
|
110
|
+
align-items: center;
|
|
99
111
|
gap: 8px;
|
|
112
|
+
padding: 12px 0 4px;
|
|
113
|
+
display: flex;
|
|
100
114
|
}
|
|
101
115
|
|
|
102
116
|
.failed {
|
|
103
|
-
|
|
117
|
+
min-width: 0;
|
|
118
|
+
color: var(--dsw-alias-label-error);
|
|
119
|
+
flex: 1;
|
|
120
|
+
margin: 0;
|
|
104
121
|
font-size: 12px;
|
|
105
|
-
|
|
122
|
+
line-height: 1.5;
|
|
106
123
|
}
|
|
107
124
|
|
|
108
125
|
.discard,
|
|
109
126
|
.save {
|
|
110
|
-
|
|
111
|
-
border-radius: 6px;
|
|
127
|
+
appearance: none;
|
|
112
128
|
font: inherit;
|
|
113
|
-
font-size: 13px;
|
|
114
129
|
cursor: pointer;
|
|
115
|
-
|
|
116
|
-
|
|
130
|
+
border: 1px solid transparent;
|
|
131
|
+
border-radius: 8px;
|
|
132
|
+
padding: 5px 14px;
|
|
133
|
+
font-size: 13px;
|
|
134
|
+
line-height: 1.5;
|
|
117
135
|
}
|
|
118
136
|
|
|
119
137
|
.discard {
|
|
120
|
-
border:
|
|
121
|
-
background: transparent;
|
|
138
|
+
border-color: var(--dsw-alias-border-l2);
|
|
122
139
|
color: var(--dsw-alias-label-secondary);
|
|
140
|
+
background: transparent;
|
|
123
141
|
}
|
|
124
142
|
|
|
125
143
|
.discard:hover:not(:disabled) {
|
|
@@ -127,156 +145,152 @@
|
|
|
127
145
|
border-color: var(--dsw-alias-label-dimmed);
|
|
128
146
|
}
|
|
129
147
|
|
|
130
|
-
.discard:active:not(:disabled) {
|
|
131
|
-
background: var(--dsw-alias-interactive-bg-hover);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
148
|
.save {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
color: var(--dsw-alias-label-primary-foreground);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
.save:hover:not(:disabled) {
|
|
141
|
-
border-color: var(--dsw-alias-button-info-hover);
|
|
142
|
-
background: var(--dsw-alias-button-info-hover);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
.save:active:not(:disabled) {
|
|
146
|
-
filter: brightness(0.94);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
.discard:focus-visible:not(:disabled),
|
|
150
|
-
.save:focus-visible:not(:disabled) {
|
|
151
|
-
outline: none;
|
|
152
|
-
box-shadow: 0 0 0 2px var(--dsw-alias-button-info-fill);
|
|
149
|
+
background: var(--dsw-alias-label-primary);
|
|
150
|
+
color: var(--dsw-alias-bg-layer-3);
|
|
153
151
|
}
|
|
154
152
|
|
|
155
153
|
.discard:disabled,
|
|
156
154
|
.save:disabled {
|
|
157
|
-
opacity: 0.
|
|
155
|
+
opacity: 0.4;
|
|
158
156
|
cursor: default;
|
|
159
157
|
}
|
|
160
158
|
|
|
159
|
+
.discard:focus-visible,
|
|
160
|
+
.save:focus-visible {
|
|
161
|
+
outline: 2px solid var(--dsw-alias-brand-primary);
|
|
162
|
+
outline-offset: 1px;
|
|
163
|
+
}
|
|
164
|
+
|
|
161
165
|
.field {
|
|
162
|
-
display: flex;
|
|
163
166
|
flex-direction: column;
|
|
164
|
-
gap:
|
|
167
|
+
gap: 6px;
|
|
168
|
+
padding: 12px 0;
|
|
169
|
+
display: flex;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.field + .field {
|
|
173
|
+
border-top: 1px solid var(--dsw-alias-border-l2);
|
|
165
174
|
}
|
|
166
175
|
|
|
167
176
|
.head {
|
|
168
|
-
display: flex;
|
|
169
177
|
align-items: center;
|
|
170
178
|
gap: 8px;
|
|
179
|
+
display: flex;
|
|
171
180
|
}
|
|
172
181
|
|
|
173
182
|
.label {
|
|
183
|
+
min-width: 0;
|
|
184
|
+
color: var(--dsw-alias-label-primary);
|
|
185
|
+
flex: 1;
|
|
174
186
|
font-size: 13px;
|
|
175
187
|
font-weight: 500;
|
|
176
|
-
|
|
188
|
+
line-height: 1.5;
|
|
177
189
|
}
|
|
178
190
|
|
|
179
191
|
.badges {
|
|
180
|
-
display: flex;
|
|
181
192
|
align-items: center;
|
|
182
|
-
gap:
|
|
193
|
+
gap: 8px;
|
|
194
|
+
display: inline-flex;
|
|
183
195
|
}
|
|
184
196
|
|
|
185
197
|
.badge {
|
|
186
|
-
|
|
187
|
-
|
|
198
|
+
white-space: nowrap;
|
|
199
|
+
background: var(--dsw-alias-bg-module-platform);
|
|
200
|
+
color: var(--dsw-alias-label-secondary);
|
|
188
201
|
border-radius: 999px;
|
|
189
|
-
|
|
190
|
-
|
|
202
|
+
padding: 1px 8px;
|
|
203
|
+
font-size: 11px;
|
|
204
|
+
font-weight: 500;
|
|
205
|
+
line-height: 17px;
|
|
191
206
|
}
|
|
192
207
|
|
|
193
208
|
.reset {
|
|
194
|
-
font
|
|
195
|
-
|
|
196
|
-
background: transparent;
|
|
197
|
-
color: var(--dsw-alias-state-business-primary);
|
|
209
|
+
font: inherit;
|
|
210
|
+
color: var(--dsw-alias-label-secondary);
|
|
198
211
|
cursor: pointer;
|
|
212
|
+
background: transparent;
|
|
213
|
+
border: none;
|
|
199
214
|
padding: 0;
|
|
200
|
-
|
|
201
|
-
|
|
215
|
+
font-size: 12px;
|
|
216
|
+
line-height: 1.5;
|
|
202
217
|
}
|
|
203
218
|
|
|
204
219
|
.reset:hover:not(:disabled) {
|
|
205
|
-
color: var(--dsw-alias-label-primary
|
|
206
|
-
text-decoration: underline;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
.reset:active:not(:disabled) {
|
|
210
|
-
color: var(--dsw-alias-state-business-primary);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
.reset:focus-visible:not(:disabled) {
|
|
214
|
-
outline: none;
|
|
215
|
-
box-shadow: 0 0 0 2px var(--dsw-alias-button-info-fill);
|
|
220
|
+
color: var(--dsw-alias-label-primary);
|
|
216
221
|
}
|
|
217
222
|
|
|
218
223
|
.reset:disabled {
|
|
219
|
-
opacity: 0.5;
|
|
220
224
|
cursor: default;
|
|
221
225
|
}
|
|
222
226
|
|
|
227
|
+
.reset:focus-visible {
|
|
228
|
+
outline: 2px solid var(--dsw-alias-brand-primary);
|
|
229
|
+
outline-offset: 2px;
|
|
230
|
+
}
|
|
231
|
+
|
|
223
232
|
.input,
|
|
224
233
|
.select {
|
|
225
|
-
padding: 6px 8px;
|
|
226
234
|
border: 1px solid var(--dsw-alias-border-l2);
|
|
227
|
-
|
|
235
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
236
|
+
height: 34px;
|
|
228
237
|
font: inherit;
|
|
229
|
-
font-size: 13px;
|
|
230
238
|
color: var(--dsw-alias-label-primary);
|
|
231
|
-
|
|
239
|
+
border-radius: 8px;
|
|
240
|
+
padding: 0 12px;
|
|
241
|
+
font-size: 13px;
|
|
242
|
+
line-height: 1.5;
|
|
232
243
|
}
|
|
233
244
|
|
|
234
|
-
.
|
|
235
|
-
|
|
236
|
-
border:
|
|
237
|
-
|
|
238
|
-
font: inherit;
|
|
239
|
-
font-size: 13px;
|
|
240
|
-
color: var(--dsw-alias-label-primary);
|
|
245
|
+
.input:focus-visible,
|
|
246
|
+
.select:focus-visible {
|
|
247
|
+
border-color: var(--dsw-alias-brand-primary);
|
|
248
|
+
outline: none;
|
|
241
249
|
}
|
|
242
250
|
|
|
243
251
|
.input:disabled,
|
|
244
252
|
.select:disabled {
|
|
245
|
-
|
|
253
|
+
color: var(--dsw-alias-label-tertiary);
|
|
254
|
+
cursor: default;
|
|
246
255
|
}
|
|
247
256
|
|
|
248
|
-
.
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
257
|
+
.inputInvalid {
|
|
258
|
+
border: 1px solid var(--dsw-alias-label-error);
|
|
259
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
260
|
+
height: 34px;
|
|
261
|
+
font: inherit;
|
|
262
|
+
color: var(--dsw-alias-label-primary);
|
|
263
|
+
border-radius: 8px;
|
|
264
|
+
padding: 0 12px;
|
|
265
|
+
font-size: 13px;
|
|
266
|
+
line-height: 1.5;
|
|
253
267
|
}
|
|
254
268
|
|
|
255
|
-
.inputInvalid:focus {
|
|
256
|
-
outline:
|
|
257
|
-
|
|
269
|
+
.inputInvalid:focus-visible {
|
|
270
|
+
outline: 2px solid var(--dsw-alias-label-error);
|
|
271
|
+
outline-offset: 1px;
|
|
272
|
+
border-color: var(--dsw-alias-label-error);
|
|
258
273
|
}
|
|
259
274
|
|
|
260
|
-
.
|
|
275
|
+
.invalid {
|
|
276
|
+
color: var(--dsw-alias-label-error);
|
|
261
277
|
margin: 0;
|
|
262
278
|
font-size: 12px;
|
|
263
|
-
|
|
279
|
+
line-height: 1.5;
|
|
264
280
|
}
|
|
265
281
|
|
|
266
|
-
.
|
|
282
|
+
.hint {
|
|
283
|
+
color: var(--dsw-alias-label-tertiary);
|
|
267
284
|
margin: 0;
|
|
268
285
|
font-size: 12px;
|
|
269
|
-
|
|
286
|
+
line-height: 1.5;
|
|
270
287
|
}
|
|
271
288
|
|
|
272
|
-
/* Reduced motion: drop the card/button transitions. Token colors and the
|
|
273
|
-
focus rings stay, so interactive states remain visible without motion. */
|
|
274
289
|
@media (prefers-reduced-motion: reduce) {
|
|
275
290
|
.card,
|
|
276
291
|
.header,
|
|
277
292
|
.chevron,
|
|
278
293
|
.chevronOpen,
|
|
279
|
-
.reset,
|
|
280
294
|
.discard,
|
|
281
295
|
.save {
|
|
282
296
|
transition: none;
|
package/src/service.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pet host service — the `pet.*` RPC domain. Owns the state machine wiring
|
|
3
|
-
* (
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `pet.
|
|
7
|
-
* for browser consumers.
|
|
3
|
+
* (projects official session events and accepts legacy `activity/status`), the
|
|
4
|
+
* affinity ledger, and the persisted display config. The API gateway maps
|
|
5
|
+
* this service's methods onto `pet.state` / `pet.interact` /
|
|
6
|
+
* `pet.setVisible` / `pet.setConfig` for browser consumers.
|
|
8
7
|
* @module @linxin666/dsh-pet/service
|
|
9
8
|
*/
|
|
10
9
|
|
|
@@ -40,6 +39,7 @@ import {
|
|
|
40
39
|
defaultPetStateConfig,
|
|
41
40
|
PetStateMachine,
|
|
42
41
|
type PetStateConfig,
|
|
42
|
+
type PetStateInput,
|
|
43
43
|
type PetStateSnapshot,
|
|
44
44
|
} from './state.ts'
|
|
45
45
|
|
|
@@ -128,6 +128,119 @@ declare module '@deepseek-ai/cordis' {
|
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
/** Runtime shape of the optional legacy activity event. */
|
|
132
|
+
interface ActivityStatusEventLike {
|
|
133
|
+
phase?: string
|
|
134
|
+
line?: string
|
|
135
|
+
phrase?: string
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Per-session facts needed to project the official event stream. */
|
|
139
|
+
interface SessionActivityRuntime {
|
|
140
|
+
activeTools: Set<string>
|
|
141
|
+
officialEventsSeen: boolean
|
|
142
|
+
stepHadFailure: boolean
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** One official event projection, optionally carrying a completed turn reward. */
|
|
146
|
+
interface PetActivityTransition {
|
|
147
|
+
input: PetStateInput
|
|
148
|
+
completedTurn?: number
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Keep tool names readable inside the compact status bubble. */
|
|
152
|
+
function displayToolName(name: string): string {
|
|
153
|
+
const compact = name.replace(/\s+/g, ' ').trim() || '工具'
|
|
154
|
+
return compact.length <= 24 ? compact : `${compact.slice(0, 21)}...`
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Whether a legacy phase is part of the pet's supported vocabulary. */
|
|
158
|
+
function isActivityPhase(phase: string): phase is PetStateInput['phase'] {
|
|
159
|
+
return ['idle', 'waiting', 'thinking', 'tool', 'review', 'done', 'failed'].includes(phase)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Project the durable DSH session vocabulary into the pet's visual phases.
|
|
164
|
+
* Unknown and log-only events do not disturb the last meaningful activity.
|
|
165
|
+
*/
|
|
166
|
+
function projectOfficialEvent(
|
|
167
|
+
event: SessionEvent,
|
|
168
|
+
runtime: SessionActivityRuntime,
|
|
169
|
+
): PetActivityTransition | undefined {
|
|
170
|
+
switch (event.type) {
|
|
171
|
+
case 'turn/start':
|
|
172
|
+
runtime.activeTools.clear()
|
|
173
|
+
runtime.stepHadFailure = false
|
|
174
|
+
return { input: { phase: 'waiting', line: '准备开始' } }
|
|
175
|
+
case 'step/start':
|
|
176
|
+
runtime.activeTools.clear()
|
|
177
|
+
runtime.stepHadFailure = false
|
|
178
|
+
return { input: { phase: 'waiting', line: '等待模型响应' } }
|
|
179
|
+
case 'assistant/chunk': {
|
|
180
|
+
const { chunk } = event.data
|
|
181
|
+
if (chunk.type === 'reasoning-delta' && chunk.text.length > 0) {
|
|
182
|
+
return { input: { phase: 'thinking', line: '正在思考' } }
|
|
183
|
+
}
|
|
184
|
+
if (chunk.type === 'text-delta' && chunk.text.length > 0) {
|
|
185
|
+
return { input: { phase: 'review', line: '整理回复中' } }
|
|
186
|
+
}
|
|
187
|
+
return undefined
|
|
188
|
+
}
|
|
189
|
+
case 'assistant/message':
|
|
190
|
+
return { input: { phase: 'review', line: '整理回复中' } }
|
|
191
|
+
case 'tool/call':
|
|
192
|
+
runtime.activeTools.add(String(event.data.callId))
|
|
193
|
+
return {
|
|
194
|
+
input: {
|
|
195
|
+
phase: 'tool',
|
|
196
|
+
line: `正在使用 ${displayToolName(event.data.name)}`,
|
|
197
|
+
},
|
|
198
|
+
}
|
|
199
|
+
case 'tool/result': {
|
|
200
|
+
const block = event.data.message.content[0]
|
|
201
|
+
runtime.activeTools.delete(String(event.data.message.source.callId))
|
|
202
|
+
runtime.stepHadFailure ||= event.data.error !== undefined || block.isError === true
|
|
203
|
+
if (runtime.activeTools.size > 0) {
|
|
204
|
+
return {
|
|
205
|
+
input: {
|
|
206
|
+
phase: 'tool',
|
|
207
|
+
line: `还有 ${runtime.activeTools.size} 个工具运行中`,
|
|
208
|
+
},
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return runtime.stepHadFailure
|
|
212
|
+
? { input: { phase: 'failed', line: '工具执行失败' } }
|
|
213
|
+
: { input: { phase: 'thinking', line: '处理工具结果' } }
|
|
214
|
+
}
|
|
215
|
+
case 'turn/end': {
|
|
216
|
+
runtime.activeTools.clear()
|
|
217
|
+
switch (event.data.reason.kind) {
|
|
218
|
+
case 'completed':
|
|
219
|
+
return {
|
|
220
|
+
input: { phase: 'done', line: '完成啦' },
|
|
221
|
+
completedTurn: event.data.turn,
|
|
222
|
+
}
|
|
223
|
+
case 'error':
|
|
224
|
+
return { input: { phase: 'failed', line: '执行失败' } }
|
|
225
|
+
case 'max-tokens':
|
|
226
|
+
return { input: { phase: 'failed', line: '达到输出上限' } }
|
|
227
|
+
case 'interrupted':
|
|
228
|
+
return { input: { phase: 'failed', line: '执行意外中断' } }
|
|
229
|
+
case 'blocked':
|
|
230
|
+
return { input: { phase: 'waiting', line: '等待继续' } }
|
|
231
|
+
case 'aborted':
|
|
232
|
+
return { input: { phase: 'idle', line: '已停止' } }
|
|
233
|
+
default:
|
|
234
|
+
// TurnEndReasonMap is merge-extensible; a newer ending must not
|
|
235
|
+
// leave the pet showing stale in-progress work.
|
|
236
|
+
return { input: { phase: 'idle' } }
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
default:
|
|
240
|
+
return undefined
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
131
244
|
/**
|
|
132
245
|
* Cordis service exposing the pet RPC domain. Lazy: nothing is scanned or
|
|
133
246
|
* written until a query or interaction arrives; event listeners update only
|
|
@@ -146,6 +259,10 @@ export class PetService extends Service {
|
|
|
146
259
|
private rewardedTurns = new Map<string, number>()
|
|
147
260
|
private enabled: boolean
|
|
148
261
|
private disposeActivity: (() => void) | undefined
|
|
262
|
+
/** Session whose most recent meaningful event currently drives the global pet. */
|
|
263
|
+
private displaySession: Session | undefined
|
|
264
|
+
private readonly sessionActivity = new WeakMap<Session, SessionActivityRuntime>()
|
|
265
|
+
private lastLegacyTurnRewardAt = 0
|
|
149
266
|
|
|
150
267
|
constructor(ctx: Context, config: PetConfig = {}) {
|
|
151
268
|
super(ctx, 'pet')
|
|
@@ -197,37 +314,38 @@ export class PetService extends Service {
|
|
|
197
314
|
this.disposeActivity = (() => {
|
|
198
315
|
const disposers = [
|
|
199
316
|
this.ctx.on('session/event', (session: Session, event: SessionEvent) => {
|
|
200
|
-
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
this.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
break
|
|
317
|
+
const runtime = this.activityRuntime(session)
|
|
318
|
+
// `activity/status` is an optional compatibility input. It is not
|
|
319
|
+
// declared as a durable event type by this package because current
|
|
320
|
+
// Harness installations publish the official session vocabulary.
|
|
321
|
+
if ((event.type as string) === 'activity/status') {
|
|
322
|
+
const payload = ((event as unknown as { data?: unknown }).data ?? {}) as ActivityStatusEventLike
|
|
323
|
+
if (typeof payload.phase !== 'string' || !isActivityPhase(payload.phase)) return
|
|
324
|
+
this.applyActivity(session, {
|
|
325
|
+
phase: payload.phase,
|
|
326
|
+
...(typeof payload.line === 'string' ? { line: payload.line } : {}),
|
|
327
|
+
...(typeof payload.phrase === 'string' ? { phrase: payload.phrase } : {}),
|
|
328
|
+
})
|
|
329
|
+
// On a legacy-only stream the compatibility event owns turn
|
|
330
|
+
// rewards. Once any official activity is observed, turn/end owns
|
|
331
|
+
// them and a derived legacy `done` cannot double-count.
|
|
332
|
+
if (payload.phase === 'done' && !runtime.officialEventsSeen) {
|
|
333
|
+
this.rewardLegacyTurn()
|
|
334
|
+
}
|
|
335
|
+
return
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const transition = projectOfficialEvent(event, runtime)
|
|
339
|
+
if (transition === undefined) return
|
|
340
|
+
runtime.officialEventsSeen = true
|
|
341
|
+
this.applyActivity(session, transition.input)
|
|
342
|
+
if (transition.completedTurn !== undefined) {
|
|
343
|
+
this.rewardTurn(String(session.id), transition.completedTurn)
|
|
228
344
|
}
|
|
229
345
|
}),
|
|
230
|
-
this.ctx.on('session/disposed', () => {
|
|
346
|
+
this.ctx.on('session/disposed', (session: Session) => {
|
|
347
|
+
if (session !== this.displaySession) return
|
|
348
|
+
this.displaySession = undefined
|
|
231
349
|
this.machine.onSessionDisposed()
|
|
232
350
|
}),
|
|
233
351
|
]
|
|
@@ -235,6 +353,27 @@ export class PetService extends Service {
|
|
|
235
353
|
})()
|
|
236
354
|
}
|
|
237
355
|
|
|
356
|
+
/** Return the projection state associated with one live session. */
|
|
357
|
+
private activityRuntime(session: Session): SessionActivityRuntime {
|
|
358
|
+
let runtime = this.sessionActivity.get(session)
|
|
359
|
+
if (runtime === undefined) {
|
|
360
|
+
runtime = {
|
|
361
|
+
activeTools: new Set(),
|
|
362
|
+
officialEventsSeen: false,
|
|
363
|
+
stepHadFailure: false,
|
|
364
|
+
}
|
|
365
|
+
this.sessionActivity.set(session, runtime)
|
|
366
|
+
}
|
|
367
|
+
return runtime
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Commit one activity as the host-global pet's most recent display state. */
|
|
371
|
+
private applyActivity(session: Session, input: PetStateInput): void {
|
|
372
|
+
this.displaySession = session
|
|
373
|
+
this.machine.onActivityStatus(input)
|
|
374
|
+
this.machine.onSessionActive()
|
|
375
|
+
}
|
|
376
|
+
|
|
238
377
|
/** RPC: pet or feed the pet. */
|
|
239
378
|
async interact(kind: PetInteraction): Promise<PetInteractResult> {
|
|
240
379
|
const nowMs = Date.now()
|
|
@@ -334,6 +473,20 @@ export class PetService extends Service {
|
|
|
334
473
|
const last = this.rewardedTurns.get(sessionId) ?? 0
|
|
335
474
|
if (turn <= last) return
|
|
336
475
|
this.rewardedTurns.set(sessionId, turn)
|
|
476
|
+
this.applyTurnReward()
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** Preserve turn rewards for installations that only emit legacy activity. */
|
|
480
|
+
private rewardLegacyTurn(): void {
|
|
481
|
+
const nowMs = Date.now()
|
|
482
|
+
// A legacy `done` snapshot may repeat during the celebration window.
|
|
483
|
+
if (nowMs - this.lastLegacyTurnRewardAt < 5_000) return
|
|
484
|
+
this.lastLegacyTurnRewardAt = nowMs
|
|
485
|
+
this.applyTurnReward()
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Persist one accepted completed-turn reward. */
|
|
489
|
+
private applyTurnReward(): void {
|
|
337
490
|
this.persist = { ...this.persist, affinity: applyTurnReward(this.persist.affinity, this.affinityConfig) }
|
|
338
491
|
this.flush()
|
|
339
492
|
}
|