sidekick-chat 0.2.9 → 0.2.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/README.md CHANGED
@@ -137,3 +137,5 @@ cd cli && node --test
137
137
 
138
138
  `test/fake-world.mjs` 起了个假的 chat World(实现上面五个接口),所以鉴权失败、发送、推流补收、
139
139
  `sidekick ask` 端到端这些都能脱网验证。
140
+
141
+ 网络断开或发送超时会保留当前消息并自动重试,确认送达后才继续后续发送;重试共用消息编号,服务端按编号去重。重试期间请勿重复输入同一句话。退出客户端会停止重试,尚未送达的消息不会跨进程保存。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sidekick-chat",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
4
4
  "description": "Sidekick 终端客户端:在终端里和秘书对话",
5
5
  "type": "module",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",
package/src/api.mjs CHANGED
@@ -5,6 +5,7 @@
5
5
  * `cortico_session` Cookie(和网页版同一个 `CORTICO_WEB_PASSWORD`)。接口本身见
6
6
  * packages/world-chat/src/world.ts 的 `startHub`。
7
7
  */
8
+ import { randomUUID } from 'node:crypto';
8
9
  import { authOrigin } from './config.mjs';
9
10
 
10
11
  /** 鉴权没过:登录态失效或令牌不对,调用方据此提示重新登录。 */
@@ -13,6 +14,10 @@ export class AuthError extends Error {}
13
14
  /** 连不上(没拿到响应):只有这种错才换备用线路,4xx / 5xx 不换。 */
14
15
  export class NetworkError extends Error {}
15
16
 
17
+ class HttpError extends Error {
18
+ constructor(status, message) { super(message); this.status = status; }
19
+ }
20
+
16
21
  const REQUEST_TIMEOUT_MS = 20_000;
17
22
 
18
23
  export class SidekickApi {
@@ -76,9 +81,15 @@ export class SidekickApi {
76
81
  if (signal?.aborted) throw signal.reason;
77
82
  throw new NetworkError(networkHint(url, err));
78
83
  }
79
- const data = await res.json().catch(() => ({}));
84
+ let data;
85
+ try { data = await res.json(); }
86
+ catch (err) {
87
+ if (signal?.aborted) throw signal.reason;
88
+ if (!res.ok) data = {};
89
+ else throw new NetworkError(networkHint(url, err));
90
+ }
80
91
  if (res.status === 401) throw new AuthError(data.error || '登录态已失效');
81
- if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
92
+ if (!res.ok) throw new HttpError(res.status, data.error || `HTTP ${res.status}`);
82
93
  return data;
83
94
  });
84
95
  }
@@ -92,8 +103,20 @@ export class SidekickApi {
92
103
  return this.request('GET', `v1/team/history?conv=${encodeURIComponent(conv)}&limit=${limit}`);
93
104
  }
94
105
 
95
- send({ conv, text, device, images }) {
96
- return this.request('POST', 'v1/team/send', { conv, text, device, images });
106
+ async send({ conv, text, device, images }, { signal, onRetry } = {}) {
107
+ const body = { conv, text, device, images, client_id: randomUUID() };
108
+ let delay = 1000;
109
+ for (;;) {
110
+ signal?.throwIfAborted();
111
+ try { return await this.request('POST', 'v1/team/send', body, { signal }); }
112
+ catch (err) {
113
+ if (!(err instanceof NetworkError) && !(err instanceof HttpError && [408, 502, 503, 504].includes(err.status))) throw err;
114
+ onRetry?.(err);
115
+ signal?.throwIfAborted();
116
+ await sleep(delay, signal);
117
+ delay = Math.min(delay * 2, 10000);
118
+ }
119
+ }
97
120
  }
98
121
 
99
122
  read(conv, seq) {
package/src/main.mjs CHANGED
@@ -553,7 +553,17 @@ async function startChat(flags) {
553
553
 
554
554
  async function sendText(text) {
555
555
  const images = pendingImages.slice();
556
- const res = await api.send({ conv: convId, text, device: settings.device, ...(images.length ? { images } : {}) });
556
+ let warned = false;
557
+ const res = await api.send({ conv: convId, text, device: settings.device, ...(images.length ? { images } : {}) }, {
558
+ signal: ctl.signal,
559
+ onRetry: () => {
560
+ if (!warned) out('消息尚未确认送达,已保留并自动重试,请勿重复发送(退出客户端会停止重试)。');
561
+ warned = true;
562
+ },
563
+ }).catch((err) => {
564
+ if (!closed) out(`未发送的消息:${text}`);
565
+ throw err;
566
+ });
557
567
  pendingImages = [];
558
568
  ui?.attachments(0);
559
569
  latest = Math.max(latest, res.seq);
@@ -570,6 +580,7 @@ async function startChat(flags) {
570
580
  const text = line.trim();
571
581
  if (!text) return;
572
582
  markPresent();
583
+ if (text === '/quit' || text === '/exit') { shutdown(0); return; }
573
584
  queue.push(() => (text.startsWith('/') ? handleSlash(text) : sendText(text)));
574
585
  void drain();
575
586
  });