opencode-oncall 0.1.0

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 (105) hide show
  1. package/LICENSE +151 -0
  2. package/README.md +50 -0
  3. package/dist/common-settings-actions.d.ts +15 -0
  4. package/dist/common-settings-actions.js +48 -0
  5. package/dist/common-settings-store.d.ts +1 -0
  6. package/dist/common-settings-store.js +1 -0
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.js +1 -0
  9. package/dist/plugin-hooks.d.ts +51 -0
  10. package/dist/plugin-hooks.js +288 -0
  11. package/dist/plugin.d.ts +10 -0
  12. package/dist/plugin.js +115 -0
  13. package/dist/settings-store.d.ts +50 -0
  14. package/dist/settings-store.js +214 -0
  15. package/dist/store-paths.d.ts +16 -0
  16. package/dist/store-paths.js +61 -0
  17. package/dist/ui/wechat-menu.d.ts +26 -0
  18. package/dist/ui/wechat-menu.js +90 -0
  19. package/dist/wechat/bind-flow.d.ts +29 -0
  20. package/dist/wechat/bind-flow.js +207 -0
  21. package/dist/wechat/bridge.d.ts +136 -0
  22. package/dist/wechat/bridge.js +1059 -0
  23. package/dist/wechat/broker-client.d.ts +23 -0
  24. package/dist/wechat/broker-client.js +274 -0
  25. package/dist/wechat/broker-endpoint.d.ts +21 -0
  26. package/dist/wechat/broker-endpoint.js +78 -0
  27. package/dist/wechat/broker-entry.d.ts +123 -0
  28. package/dist/wechat/broker-entry.js +1321 -0
  29. package/dist/wechat/broker-launcher.d.ts +37 -0
  30. package/dist/wechat/broker-launcher.js +418 -0
  31. package/dist/wechat/broker-mutation-queue.d.ts +93 -0
  32. package/dist/wechat/broker-mutation-queue.js +126 -0
  33. package/dist/wechat/broker-server.d.ts +86 -0
  34. package/dist/wechat/broker-server.js +1340 -0
  35. package/dist/wechat/broker-state-store.d.ts +335 -0
  36. package/dist/wechat/broker-state-store.js +1964 -0
  37. package/dist/wechat/command-parser.d.ts +18 -0
  38. package/dist/wechat/command-parser.js +58 -0
  39. package/dist/wechat/compat/jiti-loader.d.ts +27 -0
  40. package/dist/wechat/compat/jiti-loader.js +118 -0
  41. package/dist/wechat/compat/openclaw-account-helpers.d.ts +29 -0
  42. package/dist/wechat/compat/openclaw-account-helpers.js +60 -0
  43. package/dist/wechat/compat/openclaw-bind-helpers.d.ts +29 -0
  44. package/dist/wechat/compat/openclaw-bind-helpers.js +169 -0
  45. package/dist/wechat/compat/openclaw-guided-smoke.d.ts +180 -0
  46. package/dist/wechat/compat/openclaw-guided-smoke.js +1134 -0
  47. package/dist/wechat/compat/openclaw-public-entry.d.ts +33 -0
  48. package/dist/wechat/compat/openclaw-public-entry.js +62 -0
  49. package/dist/wechat/compat/openclaw-public-helpers.d.ts +70 -0
  50. package/dist/wechat/compat/openclaw-public-helpers.js +68 -0
  51. package/dist/wechat/compat/openclaw-qr-gateway.d.ts +15 -0
  52. package/dist/wechat/compat/openclaw-qr-gateway.js +39 -0
  53. package/dist/wechat/compat/openclaw-smoke.d.ts +48 -0
  54. package/dist/wechat/compat/openclaw-smoke.js +100 -0
  55. package/dist/wechat/compat/openclaw-sync-buf.d.ts +24 -0
  56. package/dist/wechat/compat/openclaw-sync-buf.js +80 -0
  57. package/dist/wechat/compat/openclaw-updates-send.d.ts +47 -0
  58. package/dist/wechat/compat/openclaw-updates-send.js +38 -0
  59. package/dist/wechat/compat/qrcode-terminal-loader.d.ts +12 -0
  60. package/dist/wechat/compat/qrcode-terminal-loader.js +16 -0
  61. package/dist/wechat/compat/slash-guard.d.ts +11 -0
  62. package/dist/wechat/compat/slash-guard.js +24 -0
  63. package/dist/wechat/dead-letter-store.d.ts +48 -0
  64. package/dist/wechat/dead-letter-store.js +224 -0
  65. package/dist/wechat/debug-bundle-collector.d.ts +49 -0
  66. package/dist/wechat/debug-bundle-collector.js +580 -0
  67. package/dist/wechat/debug-bundle-flow.d.ts +37 -0
  68. package/dist/wechat/debug-bundle-flow.js +180 -0
  69. package/dist/wechat/debug-bundle-redaction.d.ts +14 -0
  70. package/dist/wechat/debug-bundle-redaction.js +339 -0
  71. package/dist/wechat/handle.d.ts +10 -0
  72. package/dist/wechat/handle.js +57 -0
  73. package/dist/wechat/ipc-auth.d.ts +6 -0
  74. package/dist/wechat/ipc-auth.js +39 -0
  75. package/dist/wechat/latest-account-state-store.d.ts +8 -0
  76. package/dist/wechat/latest-account-state-store.js +38 -0
  77. package/dist/wechat/notification-dispatcher.d.ts +34 -0
  78. package/dist/wechat/notification-dispatcher.js +266 -0
  79. package/dist/wechat/notification-format.d.ts +15 -0
  80. package/dist/wechat/notification-format.js +196 -0
  81. package/dist/wechat/notification-store.d.ts +72 -0
  82. package/dist/wechat/notification-store.js +807 -0
  83. package/dist/wechat/notification-types.d.ts +37 -0
  84. package/dist/wechat/notification-types.js +1 -0
  85. package/dist/wechat/openclaw-account-adapter.d.ts +30 -0
  86. package/dist/wechat/openclaw-account-adapter.js +60 -0
  87. package/dist/wechat/operator-store.d.ts +9 -0
  88. package/dist/wechat/operator-store.js +69 -0
  89. package/dist/wechat/protocol.d.ts +150 -0
  90. package/dist/wechat/protocol.js +197 -0
  91. package/dist/wechat/question-interaction.d.ts +24 -0
  92. package/dist/wechat/question-interaction.js +180 -0
  93. package/dist/wechat/request-store.d.ts +108 -0
  94. package/dist/wechat/request-store.js +669 -0
  95. package/dist/wechat/session-digest.d.ts +50 -0
  96. package/dist/wechat/session-digest.js +167 -0
  97. package/dist/wechat/state-paths.d.ts +26 -0
  98. package/dist/wechat/state-paths.js +92 -0
  99. package/dist/wechat/status-format.d.ts +26 -0
  100. package/dist/wechat/status-format.js +616 -0
  101. package/dist/wechat/token-store.d.ts +20 -0
  102. package/dist/wechat/token-store.js +193 -0
  103. package/dist/wechat/wechat-status-runtime.d.ts +89 -0
  104. package/dist/wechat/wechat-status-runtime.js +518 -0
  105. package/package.json +74 -0
@@ -0,0 +1,38 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { ensureWechatStateLayout, latestAccountStatePath, WECHAT_FILE_MODE } from "./state-paths.js";
3
+ function normalize(value) {
4
+ if (!value || typeof value !== "object") {
5
+ return null;
6
+ }
7
+ const input = value;
8
+ const accountId = typeof input.accountId === "string" && input.accountId.trim().length > 0 ? input.accountId : null;
9
+ const token = typeof input.token === "string" && input.token.trim().length > 0 ? input.token : null;
10
+ const baseUrl = typeof input.baseUrl === "string" && input.baseUrl.trim().length > 0 ? input.baseUrl : null;
11
+ if (!accountId || !token || !baseUrl) {
12
+ return null;
13
+ }
14
+ return {
15
+ accountId,
16
+ token,
17
+ baseUrl,
18
+ ...(typeof input.getUpdatesBuf === "string" && input.getUpdatesBuf.trim().length > 0 ? { getUpdatesBuf: input.getUpdatesBuf } : {}),
19
+ };
20
+ }
21
+ export async function readWechatLatestAccountState() {
22
+ try {
23
+ const raw = await readFile(latestAccountStatePath(), "utf8");
24
+ return normalize(JSON.parse(raw));
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ }
30
+ export async function writeWechatLatestAccountState(input) {
31
+ await ensureWechatStateLayout();
32
+ const normalized = normalize(input);
33
+ if (!normalized) {
34
+ throw new Error("invalid latest account state");
35
+ }
36
+ await writeFile(latestAccountStatePath(), `${JSON.stringify(normalized, null, 2)}\n`, { encoding: "utf8", mode: WECHAT_FILE_MODE });
37
+ return normalized;
38
+ }
@@ -0,0 +1,34 @@
1
+ import { listPendingNotifications, markNotificationFailed, markNotificationResolved, markNotificationSent, purgeTerminalNotificationsBefore } from "./notification-store.js";
2
+ import type { NotificationKind, NotificationRecord } from "./notification-types.js";
3
+ export type WechatNotificationSendInput = {
4
+ to: string;
5
+ text: string;
6
+ contextToken?: string;
7
+ };
8
+ export type WechatNotificationDeliveryFailureInput = {
9
+ kind: NotificationKind;
10
+ requestKind?: NotificationRecord["requestKind"];
11
+ routeKey?: string;
12
+ scopeKey?: string;
13
+ wechatAccountId: string;
14
+ userId: string;
15
+ registrationEpoch?: string;
16
+ };
17
+ type NotificationStateOps = {
18
+ listPendingNotifications: typeof listPendingNotifications;
19
+ markNotificationResolved: typeof markNotificationResolved;
20
+ markNotificationFailed: typeof markNotificationFailed;
21
+ markNotificationSent: typeof markNotificationSent;
22
+ purgeTerminalNotificationsBefore: typeof purgeTerminalNotificationsBefore;
23
+ };
24
+ type CreateWechatNotificationDispatcherInput = {
25
+ sendMessage: (input: WechatNotificationSendInput) => Promise<unknown>;
26
+ onDeliveryFailed?: (input: WechatNotificationDeliveryFailureInput) => Promise<void> | void;
27
+ notificationStateOps?: Partial<NotificationStateOps>;
28
+ };
29
+ type WechatNotificationDispatcher = {
30
+ drainOutboundMessages: () => Promise<void>;
31
+ };
32
+ export declare function suppressPreparedPendingNotifications(records: NotificationRecord[]): Promise<void>;
33
+ export declare function createWechatNotificationDispatcher(input: CreateWechatNotificationDispatcherInput): WechatNotificationDispatcher;
34
+ export {};
@@ -0,0 +1,266 @@
1
+ import { readWechatSettingsStore } from "../settings-store.js";
2
+ import { loadBrokerStateStoreSnapshot, readBrokerDeliveryToken, readBrokerIndexedRequest, } from "./broker-state-store.js";
3
+ import { formatWechatNotificationText } from "./notification-format.js";
4
+ import { listPendingNotifications, markNotificationFailed, markNotificationResolved, markNotificationSent, purgeTerminalNotificationsBefore, } from "./notification-store.js";
5
+ const DEFAULT_NOTIFICATION_TERMINAL_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
6
+ function toPositiveNumber(rawValue, fallback) {
7
+ if (typeof rawValue !== "string" || rawValue.trim().length === 0) {
8
+ return fallback;
9
+ }
10
+ const parsed = Number(rawValue);
11
+ if (!Number.isFinite(parsed) || parsed <= 0) {
12
+ return fallback;
13
+ }
14
+ return parsed;
15
+ }
16
+ function shouldSendRecord(record, notifications) {
17
+ if (!notifications.enabled) {
18
+ return false;
19
+ }
20
+ if (record.kind === "question") {
21
+ return notifications.question;
22
+ }
23
+ if (record.kind === "permission") {
24
+ return notifications.permission;
25
+ }
26
+ if (record.kind === "requestTerminal") {
27
+ return record.requestKind === "permission"
28
+ ? notifications.permission
29
+ : notifications.question;
30
+ }
31
+ if (record.kind === "naturalStop") {
32
+ return notifications.sessionError;
33
+ }
34
+ if (record.kind === "sessionError" && record.source === "retryError") {
35
+ return notifications.retryError;
36
+ }
37
+ return notifications.sessionError;
38
+ }
39
+ function toErrorMessage(error) {
40
+ if (error instanceof Error) {
41
+ return error.message;
42
+ }
43
+ if (typeof error === "string") {
44
+ return error;
45
+ }
46
+ return String(error);
47
+ }
48
+ function isNotPendingStateError(error) {
49
+ if (!(error instanceof Error)) {
50
+ return false;
51
+ }
52
+ return /not pending/i.test(error.message);
53
+ }
54
+ function isNotSuppressibleStateError(error) {
55
+ if (!(error instanceof Error)) {
56
+ return false;
57
+ }
58
+ return /not pending|neither pending nor sent/i.test(error.message);
59
+ }
60
+ async function shouldSuppressPendingNotification(record) {
61
+ if (record.kind === "sessionError") {
62
+ const tokenState = await readBrokerDeliveryToken({
63
+ wechatAccountId: record.wechatAccountId,
64
+ userId: record.userId,
65
+ }).catch(() => undefined);
66
+ return Boolean(tokenState &&
67
+ !tokenState.staleReason &&
68
+ tokenState.updatedAt > record.createdAt);
69
+ }
70
+ if (record.kind === "requestTerminal") {
71
+ return false;
72
+ }
73
+ if (record.kind === "naturalStop") {
74
+ if (typeof record.handle !== "string" ||
75
+ record.handle.trim().length === 0) {
76
+ return false;
77
+ }
78
+ const snapshot = await loadBrokerStateStoreSnapshot().catch(() => undefined);
79
+ if (!snapshot) {
80
+ return false;
81
+ }
82
+ const active = snapshot?.active?.naturalStops?.[record.handle];
83
+ if (!active || typeof active !== "object") {
84
+ return true;
85
+ }
86
+ if (typeof record.scopeKey === "string" &&
87
+ record.scopeKey.trim().length > 0) {
88
+ const activeScopeKey = active.scopeKey ??
89
+ active.instanceID;
90
+ return activeScopeKey !== record.scopeKey;
91
+ }
92
+ return false;
93
+ }
94
+ if (typeof record.routeKey !== "string" ||
95
+ record.routeKey.trim().length === 0) {
96
+ return false;
97
+ }
98
+ const request = await readBrokerIndexedRequest({
99
+ kind: record.kind,
100
+ routeKey: record.routeKey,
101
+ });
102
+ if (!request) {
103
+ return true;
104
+ }
105
+ return request.status !== "open";
106
+ }
107
+ function isNotFailWritableStateError(error) {
108
+ if (!(error instanceof Error)) {
109
+ return false;
110
+ }
111
+ return /not pending/i.test(error.message);
112
+ }
113
+ export async function suppressPreparedPendingNotifications(records) {
114
+ for (const record of records) {
115
+ try {
116
+ await markNotificationResolved({
117
+ idempotencyKey: record.idempotencyKey,
118
+ resolvedAt: Date.now(),
119
+ suppressed: true,
120
+ });
121
+ }
122
+ catch (error) {
123
+ if (!isNotSuppressibleStateError(error)) {
124
+ throw error;
125
+ }
126
+ }
127
+ }
128
+ }
129
+ export function createWechatNotificationDispatcher(input) {
130
+ const notificationStateOps = {
131
+ listPendingNotifications,
132
+ markNotificationResolved,
133
+ markNotificationFailed,
134
+ markNotificationSent,
135
+ purgeTerminalNotificationsBefore,
136
+ ...input.notificationStateOps,
137
+ };
138
+ const inFlightNotificationIds = new Set();
139
+ return {
140
+ drainOutboundMessages: async () => {
141
+ const retentionMs = toPositiveNumber(process.env.WECHAT_NOTIFICATION_TERMINAL_RETENTION_MS, DEFAULT_NOTIFICATION_TERMINAL_RETENTION_MS);
142
+ await notificationStateOps.purgeTerminalNotificationsBefore({
143
+ cutoffAt: Date.now() - retentionMs,
144
+ });
145
+ const settings = await readWechatSettingsStore();
146
+ const notifications = settings.wechat?.notifications;
147
+ const targetUserId = settings.wechat?.primaryBinding?.userId;
148
+ const targetAccountId = settings.wechat?.primaryBinding?.accountId;
149
+ if (!notifications) {
150
+ return;
151
+ }
152
+ if (typeof targetUserId !== "string" ||
153
+ targetUserId.trim().length === 0) {
154
+ return;
155
+ }
156
+ if (typeof targetAccountId !== "string" ||
157
+ targetAccountId.trim().length === 0) {
158
+ return;
159
+ }
160
+ const pending = await notificationStateOps.listPendingNotifications();
161
+ for (const record of pending) {
162
+ if (inFlightNotificationIds.has(record.idempotencyKey)) {
163
+ continue;
164
+ }
165
+ inFlightNotificationIds.add(record.idempotencyKey);
166
+ try {
167
+ if (await shouldSuppressPendingNotification(record)) {
168
+ try {
169
+ await notificationStateOps.markNotificationResolved({
170
+ idempotencyKey: record.idempotencyKey,
171
+ resolvedAt: Date.now(),
172
+ suppressed: true,
173
+ });
174
+ }
175
+ catch (error) {
176
+ if (!isNotSuppressibleStateError(error)) {
177
+ throw error;
178
+ }
179
+ }
180
+ continue;
181
+ }
182
+ if (!shouldSendRecord(record, notifications)) {
183
+ continue;
184
+ }
185
+ if (record.userId !== targetUserId ||
186
+ record.wechatAccountId !== targetAccountId) {
187
+ continue;
188
+ }
189
+ const tokenState = await readBrokerDeliveryToken({
190
+ wechatAccountId: record.wechatAccountId,
191
+ userId: record.userId,
192
+ }).catch(() => undefined);
193
+ if (tokenState?.staleReason) {
194
+ continue;
195
+ }
196
+ try {
197
+ await input.sendMessage({
198
+ to: targetUserId,
199
+ text: formatWechatNotificationText(record),
200
+ ...(tokenState && !tokenState.staleReason
201
+ ? { contextToken: tokenState.contextToken }
202
+ : {}),
203
+ });
204
+ }
205
+ catch (error) {
206
+ let markFailedError;
207
+ let persistedFailed = false;
208
+ try {
209
+ await notificationStateOps.markNotificationFailed({
210
+ idempotencyKey: record.idempotencyKey,
211
+ failedAt: Date.now(),
212
+ reason: toErrorMessage(error),
213
+ });
214
+ persistedFailed = true;
215
+ }
216
+ catch (markError) {
217
+ if (!isNotFailWritableStateError(markError)) {
218
+ markFailedError = markError;
219
+ }
220
+ }
221
+ if (persistedFailed) {
222
+ await input.onDeliveryFailed?.({
223
+ kind: record.kind,
224
+ requestKind: record.requestKind,
225
+ routeKey: record.routeKey,
226
+ scopeKey: record.scopeKey,
227
+ wechatAccountId: record.wechatAccountId,
228
+ userId: record.userId,
229
+ registrationEpoch: record.registrationEpoch,
230
+ });
231
+ }
232
+ if (markFailedError) {
233
+ throw markFailedError;
234
+ }
235
+ continue;
236
+ }
237
+ try {
238
+ await notificationStateOps.markNotificationSent({
239
+ idempotencyKey: record.idempotencyKey,
240
+ sentAt: Date.now(),
241
+ });
242
+ }
243
+ catch (error) {
244
+ if (!isNotPendingStateError(error)) {
245
+ try {
246
+ await notificationStateOps.markNotificationFailed({
247
+ idempotencyKey: record.idempotencyKey,
248
+ failedAt: Date.now(),
249
+ reason: `notification delivered but sent persistence failed: ${toErrorMessage(error)}`,
250
+ });
251
+ }
252
+ catch (markFailedError) {
253
+ if (!isNotFailWritableStateError(markFailedError)) {
254
+ throw markFailedError;
255
+ }
256
+ }
257
+ }
258
+ }
259
+ }
260
+ finally {
261
+ inFlightNotificationIds.delete(record.idempotencyKey);
262
+ }
263
+ }
264
+ },
265
+ };
266
+ }
@@ -0,0 +1,15 @@
1
+ import type { NaturalStopTerminalReason, NotificationRecord } from "./notification-types.js";
2
+ import type { BrokerLegacyHandleClosure } from "./broker-state-store.js";
3
+ import type { RequestTerminalReason } from "./request-store.js";
4
+ export declare function formatNaturalStopClosedText(input: {
5
+ handle?: string;
6
+ terminalReason?: NaturalStopTerminalReason;
7
+ }): string;
8
+ export declare function formatTerminalRequestClosedText(input: {
9
+ requestKind: "question" | "permission";
10
+ handle?: string;
11
+ terminalReason?: RequestTerminalReason;
12
+ replacementHandle?: string;
13
+ }): string;
14
+ export declare function formatBrokerLegacyHandleClosureText(input: Pick<BrokerLegacyHandleClosure, "kind" | "handle" | "reason" | "message" | "replacementHandle">): string;
15
+ export declare function formatWechatNotificationText(record: NotificationRecord): string;
@@ -0,0 +1,196 @@
1
+ function formatHandle(handle, fallback) {
2
+ if (typeof handle === "string" && handle.trim().length > 0) {
3
+ return handle;
4
+ }
5
+ return fallback;
6
+ }
7
+ function formatQuestionType(mode) {
8
+ if (mode === "multiple")
9
+ return "多选";
10
+ if (mode === "single")
11
+ return "单选";
12
+ return "文本";
13
+ }
14
+ function formatQuestionOptions(options = []) {
15
+ return options.flatMap((option) => [
16
+ `${option.index}. ${option.label}`,
17
+ ...(typeof option.description === "string" && option.description.trim().length > 0 ? [option.description.trim()] : []),
18
+ ]);
19
+ }
20
+ function formatQuestionReplyExamples(handle, mode, allowCustom) {
21
+ const examples = [];
22
+ if (mode === "single") {
23
+ examples.push(`/reply ${handle} 1`);
24
+ }
25
+ if (mode === "multiple") {
26
+ examples.push(`/reply ${handle} 1,2`);
27
+ }
28
+ if (mode === "text" || allowCustom) {
29
+ examples.push(`/reply ${handle} 你的自定义回答`);
30
+ }
31
+ if (mode === "multiple" && allowCustom) {
32
+ examples.push(`/reply ${handle} 1,3; 其他:先灰度再全量`);
33
+ }
34
+ return examples;
35
+ }
36
+ function formatPermissionReplySemantics() {
37
+ return [
38
+ "once:仅处理这一次",
39
+ "always:后续同类请求自动允许",
40
+ "reject:拒绝当前请求",
41
+ ];
42
+ }
43
+ function formatTerminalReasonLabel(reason) {
44
+ if (reason === "answered")
45
+ return "已在电脑端回复";
46
+ if (reason === "handled")
47
+ return "已在电脑端处理";
48
+ if (reason === "rejected")
49
+ return "已在电脑端拒绝";
50
+ if (reason === "expired")
51
+ return "已过期";
52
+ if (reason === "replaced")
53
+ return "已被新入口替代";
54
+ return "已结束";
55
+ }
56
+ function formatTerminalRefusalLabel(requestKind) {
57
+ return requestKind === "permission" ? "该入口不再接受权限处理。" : "该入口不再接受回复。";
58
+ }
59
+ function formatNaturalStopTerminalReasonLabel(reason) {
60
+ if (reason === "replied")
61
+ return "已在微信端补充回复";
62
+ if (reason === "continued")
63
+ return "已在电脑端继续处理";
64
+ if (reason === "expired")
65
+ return "已过期";
66
+ return "已结束";
67
+ }
68
+ function toRequestTerminalReason(reason) {
69
+ if (reason === "answered" || reason === "handled" || reason === "rejected" || reason === "expired" || reason === "replaced") {
70
+ return reason;
71
+ }
72
+ return undefined;
73
+ }
74
+ function toNaturalStopTerminalReason(reason) {
75
+ if (reason === "replied" || reason === "continued" || reason === "expired") {
76
+ return reason;
77
+ }
78
+ return undefined;
79
+ }
80
+ export function formatNaturalStopClosedText(input) {
81
+ const handle = formatHandle(input.handle, "s?");
82
+ return [
83
+ `中止通知 ${handle} 已结束`,
84
+ `原因:${formatNaturalStopTerminalReasonLabel(input.terminalReason)}`,
85
+ "说明:该入口不再接受回复。",
86
+ ].join("\n");
87
+ }
88
+ function formatSessionErrorText(record) {
89
+ if (typeof record.action !== "string"
90
+ || record.action.trim().length === 0
91
+ || typeof record.redactedSummary !== "string"
92
+ || record.redactedSummary.trim().length === 0
93
+ || typeof record.severityAdvice !== "string"
94
+ || record.severityAdvice.trim().length === 0) {
95
+ return "检测到会话异常(retry),请在 OpenCode 中检查并处理。";
96
+ }
97
+ return [
98
+ `检测到会话异常(${record.sessionID?.trim() || "session?"})`,
99
+ `动作:${record.action.trim()}`,
100
+ `原因摘要:${record.redactedSummary.trim()}`,
101
+ `处理建议:${record.severityAdvice.trim()}`,
102
+ ].join("\n");
103
+ }
104
+ function formatNaturalStopText(record) {
105
+ if (record.naturalStopTerminalReason) {
106
+ return formatNaturalStopClosedText({
107
+ handle: record.handle,
108
+ terminalReason: record.naturalStopTerminalReason,
109
+ });
110
+ }
111
+ const handle = formatHandle(record.handle, "s?");
112
+ return [
113
+ `会话已自然中止(${handle})`,
114
+ `原因摘要:${record.redactedSummary?.trim() || "原因摘要不可安全展示"}`,
115
+ `处理建议:${record.severityAdvice?.trim() || "已停止并等待你的回复"}`,
116
+ `/reply ${handle} 你的补充内容`,
117
+ "发送后会把补充说明回到当前会话。",
118
+ ].join("\n");
119
+ }
120
+ export function formatTerminalRequestClosedText(input) {
121
+ const handle = formatHandle(input.handle, input.requestKind === "permission" ? "p?" : "q?");
122
+ const lines = [
123
+ `${input.requestKind === "permission" ? "权限" : "问题"}入口 ${handle} 已结束`,
124
+ `原因:${formatTerminalReasonLabel(input.terminalReason)}`,
125
+ `说明:${formatTerminalRefusalLabel(input.requestKind)}`,
126
+ ...(input.terminalReason === "replaced" && typeof input.replacementHandle === "string" && input.replacementHandle.trim().length > 0
127
+ ? [`请改用新入口:${input.replacementHandle.trim()}`]
128
+ : []),
129
+ ];
130
+ return lines.join("\n");
131
+ }
132
+ export function formatBrokerLegacyHandleClosureText(input) {
133
+ if (typeof input.message === "string" && input.message.trim().length > 0) {
134
+ return input.message.trim();
135
+ }
136
+ if (input.kind === "naturalStop") {
137
+ return formatNaturalStopClosedText({
138
+ handle: input.handle,
139
+ terminalReason: toNaturalStopTerminalReason(input.reason),
140
+ });
141
+ }
142
+ return formatTerminalRequestClosedText({
143
+ requestKind: input.kind,
144
+ handle: input.handle,
145
+ terminalReason: toRequestTerminalReason(input.reason),
146
+ replacementHandle: input.replacementHandle,
147
+ });
148
+ }
149
+ export function formatWechatNotificationText(record) {
150
+ if (record.kind === "question") {
151
+ const handle = formatHandle(record.handle, "q?");
152
+ const prompt = record.prompt;
153
+ if (prompt && "mode" in prompt) {
154
+ const lines = [
155
+ `收到新的问题请求(${handle})`,
156
+ prompt.title ?? prompt.body ?? "请在 OpenCode 中处理该问题。",
157
+ prompt.body && prompt.title ? prompt.body : undefined,
158
+ `类型:${formatQuestionType(prompt.mode)}`,
159
+ ...formatQuestionOptions(prompt.options),
160
+ ...formatQuestionReplyExamples(handle, prompt.mode, prompt.custom === true),
161
+ ].filter(Boolean);
162
+ return lines.join("\n");
163
+ }
164
+ return `收到新的问题请求(${handle}),请在 OpenCode 中处理。`;
165
+ }
166
+ if (record.kind === "permission") {
167
+ const handle = formatHandle(record.handle, "p?");
168
+ const prompt = record.prompt;
169
+ if (prompt && !('mode' in prompt)) {
170
+ const lines = [
171
+ `收到新的权限请求(${handle})`,
172
+ prompt.title ?? "请在 OpenCode 中处理该权限请求。",
173
+ `类型:${prompt.type ?? "unknown"}`,
174
+ prompt.description,
175
+ `/allow ${handle} once`,
176
+ `/allow ${handle} always`,
177
+ `/allow ${handle} reject`,
178
+ ...formatPermissionReplySemantics(),
179
+ ].filter(Boolean);
180
+ return lines.join("\n");
181
+ }
182
+ return `收到新的权限请求(${handle}),请在 OpenCode 中处理。`;
183
+ }
184
+ if (record.kind === "requestTerminal") {
185
+ return formatTerminalRequestClosedText({
186
+ requestKind: record.requestKind === "permission" ? "permission" : "question",
187
+ handle: record.handle,
188
+ terminalReason: record.terminalReason,
189
+ replacementHandle: record.replacementHandle,
190
+ });
191
+ }
192
+ if (record.kind === "naturalStop") {
193
+ return formatNaturalStopText(record);
194
+ }
195
+ return formatSessionErrorText(record);
196
+ }
@@ -0,0 +1,72 @@
1
+ import type { NaturalStopTerminalReason, NotificationRecord, SessionReplyTarget } from "./notification-types.js";
2
+ type NotificationStoreTestHooks = {
3
+ beforePersistBackfilledScopeKey?: (input: {
4
+ record: NotificationRecord;
5
+ scopeKey: string;
6
+ }) => Promise<void> | void;
7
+ afterWriteNotification?: (record: NotificationRecord) => Promise<void> | void;
8
+ };
9
+ export declare function setNotificationStoreTestHooks(hooks: NotificationStoreTestHooks | undefined): void;
10
+ export declare function upsertNotification(input: Omit<NotificationRecord, "status" | "sentAt" | "resolvedAt" | "failedAt" | "suppressedAt" | "failureReason">, options?: {
11
+ initialStatus?: "pending" | "suppressed";
12
+ suppressedAt?: number;
13
+ }): Promise<NotificationRecord>;
14
+ export declare function markNotificationSent(input: {
15
+ idempotencyKey: string;
16
+ sentAt: number;
17
+ }): Promise<NotificationRecord>;
18
+ export declare function markNotificationResolved(input: {
19
+ idempotencyKey: string;
20
+ resolvedAt: number;
21
+ suppressed?: boolean;
22
+ }): Promise<NotificationRecord>;
23
+ export declare function markNotificationFailed(input: {
24
+ idempotencyKey: string;
25
+ failedAt: number;
26
+ reason: string;
27
+ }): Promise<NotificationRecord>;
28
+ export declare function listPendingNotifications(): Promise<NotificationRecord[]>;
29
+ export declare function findMergeableNotification(input: {
30
+ kind: "question" | "permission";
31
+ routeKey: string;
32
+ handle: string;
33
+ scopeKey: string;
34
+ createdAt: number;
35
+ excludeIdempotencyKey?: string;
36
+ }): Promise<NotificationRecord | undefined>;
37
+ export declare function findSentNotificationByRequest(input: {
38
+ kind: "question" | "permission";
39
+ routeKey: string;
40
+ handle: string;
41
+ }): Promise<NotificationRecord | undefined>;
42
+ export declare function listActiveNaturalStops(input?: {
43
+ wechatAccountId?: string;
44
+ userId?: string;
45
+ scopeKey?: string;
46
+ }): Promise<NotificationRecord[]>;
47
+ export declare function listRetainedNaturalStopHandles(): Promise<string[]>;
48
+ export declare function findActiveNaturalStopByReplyTarget(input: {
49
+ replyTarget: SessionReplyTarget;
50
+ wechatAccountId?: string;
51
+ userId?: string;
52
+ }): Promise<NotificationRecord | undefined>;
53
+ export declare function findActiveNaturalStopByHandle(input: {
54
+ handle: string;
55
+ }): Promise<NotificationRecord | undefined>;
56
+ export declare function findTerminalNaturalStopByHandle(input: {
57
+ handle: string;
58
+ }): Promise<NotificationRecord | undefined>;
59
+ export declare function listActiveNaturalStopsForScope(input: {
60
+ scopeKey: string;
61
+ wechatAccountId?: string;
62
+ userId?: string;
63
+ }): Promise<NotificationRecord[]>;
64
+ export declare function markNaturalStopTerminal(input: {
65
+ idempotencyKey: string;
66
+ resolvedAt: number;
67
+ terminalReason: NaturalStopTerminalReason;
68
+ }): Promise<NotificationRecord>;
69
+ export declare function purgeTerminalNotificationsBefore(input: {
70
+ cutoffAt: number;
71
+ }): Promise<number>;
72
+ export {};