@keyou007/agent-bot 0.1.22 → 0.1.23

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 (47) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +12 -2
  3. package/README.zh.md +12 -2
  4. package/config.example.yaml +5 -0
  5. package/dist/config/loadConfig.js +4 -0
  6. package/dist/config/loadConfig.js.map +1 -1
  7. package/dist/config/schema.js +1 -0
  8. package/dist/config/schema.js.map +1 -1
  9. package/dist/feishu/CardRenderer.js +31 -2
  10. package/dist/feishu/CardRenderer.js.map +1 -1
  11. package/dist/feishu/FeishuMessageClient.js +2 -2
  12. package/dist/feishu/FeishuMessageClient.js.map +1 -1
  13. package/dist/index.js +44 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/local-files/TurnPreviewPage.js +106 -87
  16. package/dist/local-files/TurnPreviewPage.js.map +1 -1
  17. package/dist/proxy/ProxySessionController.js +12 -2
  18. package/dist/proxy/ProxySessionController.js.map +1 -1
  19. package/dist/state/StateStore.js +25 -0
  20. package/dist/state/StateStore.js.map +1 -1
  21. package/dist/state/migrations.js +12 -0
  22. package/dist/state/migrations.js.map +1 -1
  23. package/dist/updates/AutomaticUpdatePreparer.js +66 -0
  24. package/dist/updates/AutomaticUpdatePreparer.js.map +1 -0
  25. package/dist/updates/DailyUpdateMonitor.js +261 -0
  26. package/dist/updates/DailyUpdateMonitor.js.map +1 -0
  27. package/dist/updates/PublishedRelease.js +43 -0
  28. package/dist/updates/PublishedRelease.js.map +1 -0
  29. package/dist/updates/types.js +2 -0
  30. package/dist/updates/types.js.map +1 -0
  31. package/npm-shrinkwrap.json +2 -2
  32. package/package.json +1 -1
  33. package/skills/agent-bot/SKILL.md +2 -0
  34. package/src/config/loadConfig.ts +4 -0
  35. package/src/config/schema.ts +4 -1
  36. package/src/feishu/CardRenderer.ts +34 -2
  37. package/src/feishu/FeishuMessageClient.ts +2 -1
  38. package/src/feishu/types.ts +1 -1
  39. package/src/index.ts +40 -0
  40. package/src/local-files/TurnPreviewPage.ts +105 -88
  41. package/src/proxy/ProxySessionController.ts +14 -2
  42. package/src/state/StateStore.ts +31 -0
  43. package/src/state/migrations.ts +12 -0
  44. package/src/updates/AutomaticUpdatePreparer.ts +76 -0
  45. package/src/updates/DailyUpdateMonitor.ts +252 -0
  46. package/src/updates/PublishedRelease.ts +44 -0
  47. package/src/updates/types.ts +19 -0
@@ -0,0 +1,252 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { Logger } from "pino";
3
+ import { compareSemver } from "../cli/SelfUpdater.js";
4
+ import { CardRenderer } from "../feishu/CardRenderer.js";
5
+ import type { CardAction, FeishuOutbound } from "../feishu/types.js";
6
+ import type { StateStore } from "../state/StateStore.js";
7
+ import { stableVersionSchema } from "./PublishedRelease.js";
8
+ import type { UpdateCheckSchedule, UpdateNotice } from "./types.js";
9
+
10
+ interface DailyUpdateOptions {
11
+ store: StateStore;
12
+ outbound: Pick<FeishuOutbound, "sendInteractiveCard" | "updateInteractiveCard">;
13
+ logger: Pick<Logger, "warn" | "info">;
14
+ currentVersion: string;
15
+ userOpenId(): string | undefined;
16
+ readLatest(): Promise<string>;
17
+ readNotes(version: string): Promise<string>;
18
+ checkSupport(): Promise<void>;
19
+ applyUpdate(version: string): Promise<void>;
20
+ hasPendingUpdate(): boolean;
21
+ pendingVersion(): string | undefined;
22
+ now?(): number;
23
+ random?(): number;
24
+ }
25
+
26
+ export function scheduleDailyUpdate(now: number, random = Math.random): UpdateCheckSchedule {
27
+ const start = new Date(now);
28
+ if (start.getHours() >= 17) start.setDate(start.getDate() + 1);
29
+ start.setHours(10, 0, 0, 0);
30
+ const end = new Date(start);
31
+ end.setHours(17, 0, 0, 0);
32
+ const lower = Math.max(now, start.getTime());
33
+ const dueAt = lower + Math.floor(Math.min(0.999999999, Math.max(0, random())) * (end.getTime() - lower));
34
+ return { day: localDay(start.getTime()), dueAt, checked: false };
35
+ }
36
+
37
+ function localDay(now: number): string {
38
+ const date = new Date(now);
39
+ return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
40
+ }
41
+
42
+ export class DailyUpdateMonitor {
43
+ private readonly renderer = new CardRenderer();
44
+ private readonly now: () => number;
45
+ private timer?: ReturnType<typeof setInterval>;
46
+ private stopped = true;
47
+ private work: Promise<void> = Promise.resolve();
48
+ private busy = false;
49
+ private cardWrites: Promise<void> = Promise.resolve();
50
+ private readonly lastCardState = new Map<string, string>();
51
+
52
+ constructor(private readonly options: DailyUpdateOptions) {
53
+ this.now = options.now ?? Date.now;
54
+ }
55
+
56
+ async start(): Promise<void> {
57
+ if (!this.stopped) return;
58
+ this.stopped = false;
59
+ this.work = this.recover().catch((error: unknown) => this.logError(error));
60
+ await this.work;
61
+ if (this.stopped) return;
62
+ this.timer = setInterval(() => this.poll(), 1_000);
63
+ this.timer.unref();
64
+ this.poll();
65
+ }
66
+
67
+ async stop(): Promise<void> {
68
+ this.stopped = true;
69
+ clearInterval(this.timer);
70
+ await this.work;
71
+ await this.cardWrites;
72
+ }
73
+
74
+ async cancel(action: CardAction): Promise<void> {
75
+ if (this.stopped) throw new Error("自动更新已停止。");
76
+ const notice = this.options.store.getUpdateNotice(String(action.value.version ?? ""));
77
+ if (!notice || !action.messageId || action.userId !== notice.userOpenId
78
+ || action.userId !== this.options.userOpenId() || action.value.token !== notice.token
79
+ || (notice.messageId && notice.messageId !== action.messageId)) {
80
+ throw new Error("更新卡片已失效或无权操作。");
81
+ }
82
+ if (notice.status === "cancelled") return;
83
+ if ((notice.status !== "countdown" && notice.status !== "announcing")
84
+ || (notice.deadline !== undefined && this.now() >= notice.deadline)) {
85
+ throw new Error("取消倒计时已结束,请查看安全重启卡片中的更新状态。");
86
+ }
87
+ this.options.store.saveUpdateNotice({ ...notice, messageId: action.messageId, status: "cancelled" });
88
+ this.options.logger.info({ version: notice.version }, "Cancelled automatic Agent Bot update.");
89
+ await this.refreshCard(notice.version);
90
+ }
91
+
92
+ async cancelPreparedUpdate(version: string): Promise<void> {
93
+ const notice = this.options.store.getUpdateNotice(version);
94
+ if (!notice || (notice.status !== "preparing" && notice.status !== "scheduled")) return;
95
+ this.options.store.saveUpdateNotice({ ...notice, status: "cancelled" });
96
+ await this.refreshCard(version).catch((error: unknown) => this.logError(error));
97
+ }
98
+
99
+ private poll(): void {
100
+ if (this.stopped || this.busy) return;
101
+ this.busy = true;
102
+ this.work = this.tick().catch((error: unknown) => this.logError(error)).finally(() => { this.busy = false; });
103
+ }
104
+
105
+ private async tick(): Promise<void> {
106
+ for (const notice of this.options.store.listPendingUpdateNotices()) {
107
+ if (this.stopped) return;
108
+ if (notice.status !== "countdown") continue;
109
+ if (notice.userOpenId !== this.options.userOpenId()) {
110
+ this.options.store.saveUpdateNotice({ ...notice, status: "cancelled" });
111
+ await this.refreshCard(notice.version);
112
+ } else if (this.now() < notice.deadline!) {
113
+ try {
114
+ await this.refreshCard(notice.version);
115
+ } catch (error) {
116
+ await this.fail(notice.version, error);
117
+ }
118
+ } else {
119
+ await this.apply(notice);
120
+ }
121
+ }
122
+ if (this.stopped) return;
123
+ const now = this.now();
124
+ let schedule = this.options.store.getUpdateCheckSchedule();
125
+ if (!schedule || schedule.day !== localDay(now)) {
126
+ // Keep tomorrow's persisted random slot when starting after the daily window.
127
+ if (!schedule || schedule.dueAt <= now) {
128
+ schedule = scheduleDailyUpdate(now, this.options.random);
129
+ this.options.store.saveUpdateCheckSchedule(schedule);
130
+ this.options.logger.info({ dueAt: new Date(schedule.dueAt).toISOString() }, "Scheduled daily Agent Bot update check.");
131
+ }
132
+ }
133
+ if (schedule.checked || now < schedule.dueAt) return;
134
+ const hour = new Date(now).getHours();
135
+ if (hour < 10 || hour >= 17) return;
136
+ const owner = this.options.userOpenId();
137
+ if (!owner || this.options.hasPendingUpdate()) return;
138
+ this.options.store.saveUpdateCheckSchedule({ ...schedule, checked: true });
139
+ const version = stableVersionSchema.parse(await this.options.readLatest());
140
+ if (this.stopped || compareSemver(version, this.options.currentVersion) <= 0
141
+ || this.options.store.getUpdateNotice(version)) return;
142
+ const notes = await this.options.readNotes(version);
143
+ if (this.stopped || owner !== this.options.userOpenId()) return;
144
+ if (!notes.trim()) throw new Error(`Release notes are unavailable for Agent Bot ${version}.`);
145
+ let supportError: string | undefined;
146
+ try {
147
+ await this.options.checkSupport();
148
+ } catch (error) {
149
+ supportError = error instanceof Error ? error.message : String(error);
150
+ }
151
+ if (this.stopped || owner !== this.options.userOpenId()) return;
152
+ await this.announce({
153
+ version, currentVersion: this.options.currentVersion, userOpenId: owner,
154
+ token: randomUUID(), notes, status: supportError ? "failed" : "announcing", error: supportError,
155
+ });
156
+ }
157
+
158
+ private async announce(notice: UpdateNotice): Promise<void> {
159
+ this.options.store.saveUpdateNotice(notice);
160
+ try {
161
+ const messageId = await this.options.outbound.sendInteractiveCard(
162
+ `open_id:${notice.userOpenId}`, this.renderer.renderUpdateNotice(notice, this.now()), notice.token,
163
+ );
164
+ if (!messageId) throw new Error("Update notification delivery was not confirmed; automatic update stopped.");
165
+ const current = this.options.store.getUpdateNotice(notice.version)!;
166
+ this.options.store.saveUpdateNotice({
167
+ ...current, messageId, status: current.status === "announcing" ? "countdown" : current.status,
168
+ deadline: current.status === "announcing" ? this.now() + 60_000 : undefined,
169
+ });
170
+ this.options.logger.info({ version: notice.version }, "Delivered private Agent Bot update notice.");
171
+ if (!this.stopped) await this.refreshCard(notice.version);
172
+ } catch (error) {
173
+ await this.fail(notice.version, error);
174
+ }
175
+ }
176
+
177
+ private async apply(notice: UpdateNotice): Promise<void> {
178
+ if (this.stopped) return;
179
+ const current = this.options.store.getUpdateNotice(notice.version)!;
180
+ if (current.status !== "countdown") return;
181
+ this.options.store.saveUpdateNotice({ ...current, status: "preparing" });
182
+ try {
183
+ await this.refreshCard(current.version);
184
+ if (this.stopped) return;
185
+ if (this.options.hasPendingUpdate()) throw new Error("已有更新或重启正在等待执行,本次自动更新已停止。");
186
+ await this.options.applyUpdate(current.version);
187
+ const latest = this.options.store.getUpdateNotice(current.version)!;
188
+ if (latest.status !== "cancelled") this.options.store.saveUpdateNotice({ ...latest, status: "scheduled" });
189
+ } catch (error) {
190
+ await this.fail(current.version, error);
191
+ return;
192
+ }
193
+ if (!this.stopped) await this.refreshCard(current.version).catch((error: unknown) => this.logError(error));
194
+ }
195
+
196
+ private async recover(): Promise<void> {
197
+ for (const notice of this.options.store.listPendingUpdateNotices()) {
198
+ if (this.stopped) return;
199
+ let recovered: UpdateNotice;
200
+ if (compareSemver(this.options.currentVersion, notice.version) >= 0) {
201
+ recovered = { ...notice, status: "completed" };
202
+ } else if (notice.userOpenId !== this.options.userOpenId()) {
203
+ recovered = { ...notice, status: "cancelled" };
204
+ } else if (notice.status === "countdown" && notice.messageId) {
205
+ // Renew the cancellation window on the same card after downtime.
206
+ recovered = { ...notice, deadline: this.now() + 60_000 };
207
+ } else if ((notice.status === "scheduled" || notice.status === "preparing") && this.options.pendingVersion() === notice.version) {
208
+ recovered = { ...notice, status: "scheduled" };
209
+ } else {
210
+ recovered = { ...notice, status: "failed", error: "上次自动更新被中断,未自动重试。" };
211
+ }
212
+ this.options.store.saveUpdateNotice(recovered);
213
+ try {
214
+ await this.refreshCard(recovered.version);
215
+ if (recovered.status === "countdown" && !this.stopped) {
216
+ const current = this.options.store.getUpdateNotice(recovered.version)!;
217
+ if (current.status === "countdown") this.options.store.saveUpdateNotice({ ...current, deadline: this.now() + 60_000 });
218
+ }
219
+ } catch (error) {
220
+ if (recovered.status === "countdown") await this.fail(recovered.version, error);
221
+ else this.logError(error);
222
+ }
223
+ }
224
+ }
225
+
226
+ private async fail(version: string, error: unknown): Promise<void> {
227
+ const notice = this.options.store.getUpdateNotice(version)!;
228
+ if (notice.status !== "cancelled") {
229
+ this.options.store.saveUpdateNotice({ ...notice, status: "failed", error: error instanceof Error ? error.message : String(error) });
230
+ }
231
+ this.logError(error);
232
+ if (!this.stopped) await this.refreshCard(version).catch((cardError: unknown) => this.logError(cardError));
233
+ }
234
+
235
+ private refreshCard(version: string): Promise<void> {
236
+ const write = this.cardWrites.then(async () => {
237
+ if (this.stopped) return;
238
+ const notice = this.options.store.getUpdateNotice(version)!;
239
+ if (!notice.messageId) return;
240
+ const key = `${notice.status}:${notice.status === "countdown" ? Math.ceil((notice.deadline! - this.now()) / 5_000) : ""}`;
241
+ if (this.lastCardState.get(version) === key) return;
242
+ await this.options.outbound.updateInteractiveCard(notice.messageId, this.renderer.renderUpdateNotice(notice, this.now()));
243
+ this.lastCardState.set(version, key);
244
+ });
245
+ this.cardWrites = write.catch(() => undefined);
246
+ return write;
247
+ }
248
+
249
+ private logError(error: unknown): void {
250
+ this.options.logger.warn({ error }, "Automatic Agent Bot update did not complete.");
251
+ }
252
+ }
@@ -0,0 +1,44 @@
1
+ import { z } from "zod";
2
+ import { AGENT_BOT_PACKAGE_NAME } from "../cli/SelfUpdater.js";
3
+
4
+ export const stableVersionSchema = z.string().regex(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/u);
5
+
6
+ export async function readLatestStableVersion(): Promise<string> {
7
+ const text = await fetchText(`https://registry.npmjs.org/${encodeURIComponent(AGENT_BOT_PACKAGE_NAME)}/latest`, 256_000);
8
+ return z.object({ version: stableVersionSchema }).parse(JSON.parse(text)).version;
9
+ }
10
+
11
+ export async function readReleaseNotes(version: string): Promise<string> {
12
+ stableVersionSchema.parse(version);
13
+ const changelog = await fetchText(
14
+ `https://raw.githubusercontent.com/keyou/agent-bot/v${version}/CHANGELOG.md`, 2_000_000,
15
+ );
16
+ return extractReleaseNotes(changelog, version);
17
+ }
18
+
19
+ export function extractReleaseNotes(changelog: string, version: string): string {
20
+ const sections = changelog.split(/^## /mu).slice(1);
21
+ const section = sections.find((value) => value.split(/\r?\n/u, 1)[0]?.startsWith(`[${version}]`));
22
+ const notes = section?.includes("\n") ? section.slice(section.indexOf("\n") + 1).trim() : undefined;
23
+ if (!notes) throw new Error(`Release notes are unavailable for Agent Bot ${version}.`);
24
+ return notes.length > 4_000 ? `${notes.slice(0, 3_900)}\n\n[完整更新日志](https://github.com/keyou/agent-bot/releases/tag/v${version})` : notes;
25
+ }
26
+
27
+ async function fetchText(url: string, maxBytes: number): Promise<string> {
28
+ const response = await fetch(url, { signal: AbortSignal.timeout(15_000) });
29
+ if (!response.ok || !response.body) throw new Error(`Release request failed: HTTP ${response.status}.`);
30
+ const reader = response.body.getReader();
31
+ const chunks: Uint8Array[] = [];
32
+ let size = 0;
33
+ try {
34
+ while (true) {
35
+ const { done, value } = await reader.read();
36
+ if (done) return Buffer.concat(chunks).toString("utf8");
37
+ size += value.byteLength;
38
+ if (size > maxBytes) throw new Error("Release response exceeds the size limit.");
39
+ chunks.push(value);
40
+ }
41
+ } finally {
42
+ await reader.cancel();
43
+ }
44
+ }
@@ -0,0 +1,19 @@
1
+ export interface UpdateCheckSchedule {
2
+ day: string;
3
+ dueAt: number;
4
+ checked: boolean;
5
+ }
6
+
7
+ export type UpdateNoticeStatus = "announcing" | "countdown" | "cancelled" | "preparing" | "scheduled" | "completed" | "failed";
8
+
9
+ export interface UpdateNotice {
10
+ version: string;
11
+ currentVersion: string;
12
+ userOpenId: string;
13
+ token: string;
14
+ notes: string;
15
+ status: UpdateNoticeStatus;
16
+ messageId?: string;
17
+ deadline?: number;
18
+ error?: string;
19
+ }