@bolloon/bolloon-agent 0.4.29 → 0.4.30

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/dist/agents/contacts/chain.js +579 -0
  2. package/dist/agents/contacts/cli.js +158 -0
  3. package/dist/agents/contacts/consent.js +124 -0
  4. package/dist/agents/contacts/grant-payload.js +45 -0
  5. package/dist/agents/contacts/grants.js +451 -0
  6. package/dist/agents/contacts/policy.js +264 -0
  7. package/dist/agents/contacts/preview-types.js +6 -0
  8. package/dist/agents/contacts/providers.js +341 -0
  9. package/dist/agents/contacts/store.js +326 -0
  10. package/dist/agents/contacts/tools.js +154 -0
  11. package/dist/agents/contacts/types.js +148 -0
  12. package/dist/agents/pi-sdk.js +10 -0
  13. package/dist/electron-build/agents/contacts/chain.js +619 -0
  14. package/dist/electron-build/agents/contacts/chain.js.map +1 -0
  15. package/dist/electron-build/agents/contacts/consent.js +163 -0
  16. package/dist/electron-build/agents/contacts/consent.js.map +1 -0
  17. package/dist/electron-build/agents/contacts/grant-payload.js +52 -0
  18. package/dist/electron-build/agents/contacts/grant-payload.js.map +1 -0
  19. package/dist/electron-build/agents/contacts/grants.js +500 -0
  20. package/dist/electron-build/agents/contacts/grants.js.map +1 -0
  21. package/dist/electron-build/agents/contacts/policy.js +271 -0
  22. package/dist/electron-build/agents/contacts/policy.js.map +1 -0
  23. package/dist/electron-build/agents/contacts/preview-types.js +8 -0
  24. package/dist/electron-build/agents/contacts/preview-types.js.map +1 -0
  25. package/dist/electron-build/agents/contacts/providers.js +384 -0
  26. package/dist/electron-build/agents/contacts/providers.js.map +1 -0
  27. package/dist/electron-build/agents/contacts/store.js +370 -0
  28. package/dist/electron-build/agents/contacts/store.js.map +1 -0
  29. package/dist/electron-build/agents/contacts/tools.js +159 -0
  30. package/dist/electron-build/agents/contacts/tools.js.map +1 -0
  31. package/dist/electron-build/agents/contacts/types.js +166 -0
  32. package/dist/electron-build/agents/contacts/types.js.map +1 -0
  33. package/dist/electron-build/agents/external-events.js.map +1 -1
  34. package/dist/electron-build/agents/goal-store.js.map +1 -1
  35. package/dist/electron-build/agents/pi-sdk.js +10 -0
  36. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  37. package/dist/electron-build/security/tool-gate.js +4 -0
  38. package/dist/electron-build/security/tool-gate.js.map +1 -1
  39. package/dist/index.js +30 -0
  40. package/dist/security/tool-gate.js +4 -0
  41. package/dist/web/mobile-contacts.js +329 -0
  42. package/dist/web/mobile-core.js +481 -15
  43. package/dist/web/mobile.html +32 -0
  44. package/dist/web/mobile.js +139 -0
  45. package/dist/web/routes-contacts.js +420 -0
  46. package/dist/web/server.js +5 -0
  47. package/package.json +1 -1
@@ -0,0 +1,619 @@
1
+ "use strict";
2
+ /**
3
+ * contacts/chain.ts — 核心链路编排 (2026-09-19, Phase 7)
4
+ *
5
+ * 这条链就是整个能力的价值所在:
6
+ * 绑定 → 受约束调用 → 结果进长期任务 → 等待回复/人工确认 → Supervisor 恢复 → 证据可回放
7
+ *
8
+ * 与既有地基的接线 (不新造轮子):
9
+ * - 等待/唤醒: `external-events.ts` 的 bindExternalWait / deliverExternalEvent / expireExternalWaits
10
+ * (source 用新加的 `contact`) —— 所以"回复只唤醒对应 Goal""重启后仍知道在等谁"直接继承既有实现。
11
+ * - Goal: `goal-store.ts` (status=awaiting_external, continuation.external, unresolvedItems)
12
+ * - Run: `run-store.ts` 的 addRunEvidence / recordStep —— 通信事实进 Run 证据
13
+ * - 审批: `consent.ts` (同 payment-approval 的形状)
14
+ *
15
+ * 硬约束 (逐条落地):
16
+ * - 发送失败 ≠ 任务完成 (只记 contact.failed, 不动 Goal 完成判据)
17
+ * - 发送成功但没回复 ≠ 目标完成 (Goal 停在 awaiting_external, 由回复/超时决定)
18
+ * - 回复来源不可信 → 不写证据、不唤醒 (contact.reply_untrusted)
19
+ * - 联系人撤销后不允许重试 (revoke 时清等待 + 记 unresolved)
20
+ * - 同一 requestId 不重复发送 (policy 的 duplicate_request + sent.json)
21
+ */
22
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ var desc = Object.getOwnPropertyDescriptor(m, k);
25
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
26
+ desc = { enumerable: true, get: function() { return m[k]; } };
27
+ }
28
+ Object.defineProperty(o, k2, desc);
29
+ }) : (function(o, m, k, k2) {
30
+ if (k2 === undefined) k2 = k;
31
+ o[k2] = m[k];
32
+ }));
33
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
34
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
35
+ }) : function(o, v) {
36
+ o["default"] = v;
37
+ });
38
+ var __importStar = (this && this.__importStar) || (function () {
39
+ var ownKeys = function(o) {
40
+ ownKeys = Object.getOwnPropertyNames || function (o) {
41
+ var ar = [];
42
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
43
+ return ar;
44
+ };
45
+ return ownKeys(o);
46
+ };
47
+ return function (mod) {
48
+ if (mod && mod.__esModule) return mod;
49
+ var result = {};
50
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
51
+ __setModuleDefault(result, mod);
52
+ return result;
53
+ };
54
+ })();
55
+ Object.defineProperty(exports, "__esModule", { value: true });
56
+ exports.maskPhone = exports.maskEmail = exports.ContactChain = void 0;
57
+ exports.contactChain = contactChain;
58
+ const goal_store_js_1 = require("../goal-store.js");
59
+ const run_store_js_1 = require("../run-store.js");
60
+ const external_events_js_1 = require("../external-events.js");
61
+ const store_js_1 = require("./store.js");
62
+ const grants_js_1 = require("./grants.js");
63
+ const consent_js_1 = require("./consent.js");
64
+ const policy_js_1 = require("./policy.js");
65
+ const providers_js_1 = require("./providers.js");
66
+ const types_js_1 = require("./types.js");
67
+ Object.defineProperty(exports, "maskEmail", { enumerable: true, get: function () { return types_js_1.maskEmail; } });
68
+ Object.defineProperty(exports, "maskPhone", { enumerable: true, get: function () { return types_js_1.maskPhone; } });
69
+ class ContactChain {
70
+ deps;
71
+ store;
72
+ otp;
73
+ consents;
74
+ /** 持久能力授权 (2026-09-19: consent 只管一次, grant 管长期) */
75
+ grants;
76
+ constructor(deps) {
77
+ this.deps = deps;
78
+ this.store = new store_js_1.ContactsStore(deps.home);
79
+ this.otp = new providers_js_1.OtpStore(this.store.dir);
80
+ this.consents = new consent_js_1.ContactConsentStore(this.store.dir);
81
+ this.grants = new grants_js_1.GrantStore(this.store.dir);
82
+ // 批准 → 真发送 (policy 会在发送前**再判一次**, 批准不等于免检)
83
+ (0, consent_js_1.setConsentExecutor)(async (c) => {
84
+ const r = await this.performSend({
85
+ contactId: c.contactId, goalId: c.goalId, runId: c.runId, requestId: c.requestId,
86
+ subject: c.subject, body: '', // body 由 pending 里的 loadBody 装载 (见下)
87
+ approvedConsentId: c.consentId,
88
+ }, /* bodyOverride */ undefined);
89
+ return { ok: r.status === 'sent' || r.status === 'awaiting_reply', result: JSON.stringify({ status: r.status, evidenceRef: r.evidenceRef }).slice(0, 200), error: r.reason };
90
+ });
91
+ }
92
+ /** 本实例操作的身份 (CLI/Web 展示与授权都要用) */
93
+ get ownerDid() { return this.deps.ownerDid; }
94
+ now() { return this.deps.now ? this.deps.now() : Date.now(); }
95
+ /** 单测/验收用: 直接拿 policy 上下文 (验证策略判定而不真发送) */
96
+ policyCtxForTest() { return this.policyCtx(); }
97
+ /** policy 上下文: store + 首次联系判定 + 待批准幂等判定 */
98
+ policyCtx() {
99
+ const grants = this.grants;
100
+ return {
101
+ store: this.store,
102
+ now: this.deps.now,
103
+ grants: {
104
+ activeFor: (ownerDid, now) => grants.activeFor(ownerDid, now),
105
+ latestFor: (ownerDid) => grants.latestFor(ownerDid),
106
+ ready: () => grants.load(),
107
+ get corrupt() { return grants.corrupt; },
108
+ },
109
+ deviceTrusted: (deviceId) => grants.deviceTrusted(deviceId),
110
+ hasPendingConsent: async (requestId) => (await this.consents.list('pending')).some((c) => c.requestId === requestId),
111
+ };
112
+ }
113
+ // ── 身份 ──────────────────────────────────────────────────────────────────
114
+ async ensureIdentity() {
115
+ return this.store.ensureIdentity(this.deps.ownerDid, this.deps.displayName || '');
116
+ }
117
+ // ── 台账 → Run/Goal 证据 (脱敏后) ──────────────────────────────────────────
118
+ async ledger(opts) {
119
+ const ts = new Date(this.now()).toISOString();
120
+ const entry = await this.store.appendLedger({ ts, ...opts });
121
+ const line = `[contact] ${opts.activity} ${opts.detail} (evidence=${entry.evidenceRef})`.slice(0, 300);
122
+ if (opts.runId) {
123
+ try {
124
+ await (0, run_store_js_1.addRunEvidence)(opts.runId, [line]);
125
+ }
126
+ catch { /* Run 不在就当外部事实 */ }
127
+ }
128
+ if (opts.goalId) {
129
+ try {
130
+ await (0, goal_store_js_1.addEvidence)(opts.goalId, [line]);
131
+ }
132
+ catch { /* 同上 */ }
133
+ }
134
+ return entry;
135
+ }
136
+ // ── 绑定 (Phase 2/3): 规范化 → 隐私展示 → 发一次验证码 → pending ────────────
137
+ async bind(opts) {
138
+ await this.store.ensureDirs();
139
+ const id = await this.ensureIdentity();
140
+ const norm = opts.kind === 'phone' ? (0, types_js_1.normalizePhone)(opts.value, opts.region || '') : (0, types_js_1.normalizeEmail)(opts.value);
141
+ if (!norm.ok)
142
+ return { ok: false, error: `联系方式不合法: ${norm.error}` };
143
+ const provider = opts.provider || (opts.kind === 'phone' ? 'local-sink' : 'local-sink');
144
+ const contact = {
145
+ contactId: (0, types_js_1.newContactId)(opts.kind),
146
+ identityId: id.identityId,
147
+ ownerDid: this.deps.ownerDid,
148
+ kind: opts.kind,
149
+ normalizedValue: norm.value,
150
+ displayValue: (0, types_js_1.displayFor)(opts.kind, norm.value),
151
+ verificationStatus: 'pending_verification',
152
+ provider,
153
+ capabilities: [],
154
+ consentScope: { kinds: [], taskRefs: [], grantedAt: '', grantedBy: '' },
155
+ secretRef: opts.secretRef || '',
156
+ limits: { ...types_js_1.DEFAULT_LIMITS },
157
+ policy: opts.policy || 'send_after_approval',
158
+ verifiedAt: null,
159
+ lastUsedAt: null,
160
+ revokedAt: null,
161
+ source: opts.source || 'user_input',
162
+ trust: opts.source === 'agent_card' ? 'unverified' : 'unverified',
163
+ aliases: opts.aliases || [],
164
+ peerDid: opts.peerDid,
165
+ note: opts.note,
166
+ createdAt: new Date(this.now()).toISOString(),
167
+ updatedAt: new Date(this.now()).toISOString(),
168
+ };
169
+ await this.store.putContact(contact);
170
+ await this.ledger({
171
+ activity: 'contact.discovered', contactId: contact.contactId,
172
+ detail: `${opts.kind} ${contact.displayValue} 已登记 (待验证, 通道 ${provider})`,
173
+ });
174
+ const { challenge, code } = await this.otp.issue({ contactId: contact.contactId, kind: opts.kind, provider, now: this.now() });
175
+ if (opts.deliverOtp === false)
176
+ return { ok: true, contact, challengeId: challenge.challengeId };
177
+ const p = (0, providers_js_1.getProvider)(provider);
178
+ const channelLabel = p?.id === 'smtp' ? 'SMTP 邮件 (真实外发)' : p?.id === 'http-webhook' ? '短信网关 HTTP (真实外发)' : '本地落盘 (未真实外发)';
179
+ const msg = {
180
+ to: norm.value, kind: opts.kind, subject: 'Bolloon 验证码',
181
+ body: `Bolloon 验证码: ${code} (10 分钟内有效, 请勿转发)`, threadToken: (0, types_js_1.newThreadToken)(), requestId: `otp-${challenge.challengeId}`,
182
+ };
183
+ const delivered = p ? await p.deliver(msg, { store: this.store, secretRef: contact.secretRef }) : { ok: false, reallySent: false, error: 'unknown_provider' };
184
+ if (!delivered.ok) {
185
+ await this.ledger({ activity: 'contact.failed', contactId: contact.contactId, detail: `验证码投递失败 (${provider}): ${String(delivered.error || '').slice(0, 120)}` });
186
+ return { ok: false, error: `验证码投递失败: ${delivered.error}`, contact, challengeId: challenge.challengeId };
187
+ }
188
+ // local-sink 是本地落盘, 用户/验收要看得到码才能继续 (真外发通道不回显)
189
+ return {
190
+ ok: true, contact, challengeId: challenge.challengeId, channelLabel,
191
+ otpForLocalSink: p?.emulatesRealSend === false ? code : undefined,
192
+ };
193
+ }
194
+ /** 验证码校验 → verified (未验证不许用于真实发送) */
195
+ async verify(opts) {
196
+ const r = await this.otp.verify(opts.challengeId, opts.code, this.now());
197
+ if (!r.ok)
198
+ return { ok: false, error: r.reason };
199
+ const c = await this.store.getContact(opts.contactId);
200
+ if (!c)
201
+ return { ok: false, error: 'contact_not_found' };
202
+ c.verificationStatus = 'verified';
203
+ c.capabilities = ['send', 'await_reply'];
204
+ c.verifiedAt = new Date(this.now()).toISOString();
205
+ await this.store.putContact(c);
206
+ await this.ledger({ activity: 'contact.approved', contactId: c.contactId, detail: `${c.kind} ${c.displayValue} 验证通过 (通道 ${c.provider})` });
207
+ return { ok: true, contact: c };
208
+ }
209
+ /** 任务级授权: 把某个联系方式授权给某个 Goal (Phase 7 的"任务绑定联系人") */
210
+ async authorizeForTask(opts) {
211
+ const c = await this.store.getContact(opts.contactId);
212
+ if (!c)
213
+ return { ok: false, error: 'contact_not_found' };
214
+ if (c.verificationStatus !== 'verified')
215
+ return { ok: false, error: 'unverified_contact' };
216
+ const taskRefs = new Set(c.consentScope?.taskRefs || []);
217
+ taskRefs.add(opts.goalId);
218
+ c.consentScope = {
219
+ kinds: opts.kinds || c.consentScope?.kinds || [],
220
+ taskRefs: [...taskRefs],
221
+ grantedAt: new Date(this.now()).toISOString(),
222
+ grantedBy: opts.by,
223
+ };
224
+ await this.store.putContact(c);
225
+ return { ok: true, contact: c };
226
+ }
227
+ // ── 预览 ──────────────────────────────────────────────────────────────────
228
+ async preview(opts) {
229
+ const decision = await (0, policy_js_1.decideContactAction)({ action: 'preview', ...opts }, this.policyCtx());
230
+ return { decision, text: decision.preview ? (0, policy_js_1.renderPreview)(decision.preview) : (decision.reason || '') };
231
+ }
232
+ // ── 发送 (唯一入口) ────────────────────────────────────────────────────────
233
+ async send(opts) {
234
+ const requestId = opts.requestId || (0, types_js_1.newRequestId)();
235
+ const decision = await (0, policy_js_1.decideContactAction)({ action: 'send', ...opts, requestId }, this.policyCtx());
236
+ if (!decision.allowed) {
237
+ await this.ledger({
238
+ activity: 'contact.denied', contactId: opts.contactId, goalId: opts.goalId, runId: opts.runId,
239
+ detail: `拒绝发送 (${decision.blockKind}): ${decision.reason}`,
240
+ });
241
+ return { status: 'denied', reason: decision.reason, blockKind: decision.blockKind, requestId, preview: decision.preview ? (0, policy_js_1.renderPreview)(decision.preview) : undefined };
242
+ }
243
+ if (decision.requiresApproval) {
244
+ const contact = await this.store.getContact(opts.contactId);
245
+ const needReason = decision.grantMissing ? 'grant_missing (没有长期授权)' : (decision.grantBlock || decision.approvalReason || 'policy');
246
+ const c = await this.consents.request({
247
+ requestId, contactId: opts.contactId,
248
+ contactName: (contact?.aliases?.[0]) || contact?.displayValue || opts.contactId,
249
+ channel: contact.kind, provider: contact.provider, reallySent: decision.reallySent,
250
+ subject: opts.subject, bodyPreview: (0, types_js_1.redactForEvidence)(opts.body).slice(0, 200),
251
+ goalId: opts.goalId, runId: opts.runId, taskRef: opts.taskKind,
252
+ reason: String(needReason), now: this.now(),
253
+ });
254
+ await this.ledger({
255
+ activity: 'contact.authorization_requested', contactId: opts.contactId, goalId: opts.goalId, runId: opts.runId,
256
+ detail: `等待人工批准 (${needReason}): ${c.contactName} · 通道 ${c.provider} · requestId=${requestId}`,
257
+ });
258
+ // 真实正文暂存到 pending/<requestId>.json (0600, 发完即删) —— consents.json / 预览里只有脱敏内容
259
+ await this.store.putPendingBody(requestId, opts.body, opts.subject, { replyExpected: opts.replyExpected !== false, replyWindowMs: opts.replyWindowMs });
260
+ // 刻意**不写** sent.json 占位: 占位会让"批准后复检"把自己判成 duplicate_request。
261
+ // 幂等改由 consent (requestId 唯一) 保证 —— 见 policy 的 hasPendingConsent。
262
+ return { status: 'awaiting_approval', consentId: c.consentId, requestId, preview: decision.preview ? (0, policy_js_1.renderPreview)(decision.preview) : undefined };
263
+ }
264
+ // 持久授权命中 → 直接进入执行 (不再创建 pending consent); 审批跳过这件事会进证据
265
+ const r = await this.performSend({
266
+ ...opts, requestId,
267
+ authorization: {
268
+ mode: decision.authorizationMode,
269
+ grantId: decision.grant?.grantId,
270
+ grantVersion: decision.grant?.version,
271
+ approvalSkipped: decision.approvalSkipped,
272
+ policyDecision: decision.policyDecision,
273
+ },
274
+ }, undefined, decision);
275
+ return r;
276
+ }
277
+ /** 真正落地的发送 (批准后也会再判一次 policy) */
278
+ async performSend(opts, bodyOverride, preset) {
279
+ const body = bodyOverride ?? opts.body;
280
+ const decision = preset && body ? preset : await (0, policy_js_1.decideContactAction)({ action: 'send', contactId: opts.contactId, goalId: opts.goalId, taskKind: opts.taskKind, subject: opts.subject, body, requestId: opts.requestId }, this.policyCtx());
281
+ if (!decision.allowed) {
282
+ await this.ledger({ activity: 'contact.denied', contactId: opts.contactId, goalId: opts.goalId, runId: opts.runId, detail: `批准后复检仍被拒 (${decision.blockKind}): ${decision.reason}` });
283
+ return { status: 'denied', reason: decision.reason, blockKind: decision.blockKind, requestId: opts.requestId };
284
+ }
285
+ const contact = await this.store.getContact(opts.contactId);
286
+ if (!contact)
287
+ return { status: 'denied', reason: 'contact_not_found', requestId: opts.requestId };
288
+ const provider = (0, providers_js_1.getProvider)(contact.provider);
289
+ if (!provider) {
290
+ await this.ledger({ activity: 'contact.failed', contactId: contact.contactId, goalId: opts.goalId, runId: opts.runId, detail: `未知通道 ${contact.provider}` });
291
+ return { status: 'failed', reason: `unknown_provider:${contact.provider}`, requestId: opts.requestId };
292
+ }
293
+ const threadToken = (0, types_js_1.newThreadToken)();
294
+ const deliver = await provider.deliver({ to: contact.normalizedValue, kind: contact.kind, subject: opts.subject, body, threadToken, requestId: opts.requestId }, { store: this.store, secretRef: contact.secretRef });
295
+ const summary = (0, types_js_1.redactForEvidence)(body).replace(/\s+/g, ' ').slice(0, 120);
296
+ if (!deliver.ok) {
297
+ await this.store.patchSend(opts.requestId, { status: 'failed', failureReason: deliver.error, errorClass: deliver.errorClass });
298
+ const ev = await this.ledger({
299
+ activity: 'contact.failed', contactId: contact.contactId, goalId: opts.goalId, runId: opts.runId,
300
+ detail: `发送失败 (${deliver.errorClass}): ${String(deliver.error || '').slice(0, 120)}`,
301
+ });
302
+ // 发送失败**不算**任务完成 —— 只记证据, 不动 Goal
303
+ return { status: 'failed', reason: deliver.error, requestId: opts.requestId, evidenceRef: ev.evidenceRef };
304
+ }
305
+ const auth = opts.authorization || (opts.approvedConsentId
306
+ ? { mode: 'one_time', approvalSkipped: false, policyDecision: 'allowed:human_approved' }
307
+ : undefined);
308
+ const rec = {
309
+ requestId: opts.requestId, contactId: contact.contactId, goalId: opts.goalId, runId: opts.runId,
310
+ kind: contact.kind, provider: contact.provider, status: 'sent',
311
+ providerMessageId: deliver.providerMessageId, threadToken, subject: opts.subject,
312
+ summary, sentAt: new Date(this.now()).toISOString(),
313
+ grantId: auth?.grantId, grantVersion: auth?.grantVersion,
314
+ authorizationMode: auth?.mode, approvalSkipped: auth?.approvalSkipped === true,
315
+ policyDecision: auth?.policyDecision,
316
+ };
317
+ // 批准路径已经预占了一条 failed 占位记录 (awaiting_approval) → 覆盖它
318
+ const prior = await this.store.findSend(opts.requestId);
319
+ if (prior)
320
+ await this.store.patchSend(opts.requestId, rec);
321
+ else
322
+ await this.store.recordSend(rec);
323
+ await this.store.markUsed(contact.contactId);
324
+ if (auth?.grantId) {
325
+ try {
326
+ await this.grants.markUsed(auth.grantId);
327
+ }
328
+ catch { /* 记不上不影响发送事实 */ }
329
+ }
330
+ if (auth?.mode !== 'one_time' && decision.forbiddenCategories.length === 0 && decision.sensitive.length > 0) {
331
+ // 敏感类别**只记类别, 不记明文** (Phase 6 审计要求)
332
+ await this.ledger({
333
+ activity: 'contact.sent', contactId: contact.contactId, goalId: opts.goalId, runId: opts.runId,
334
+ detail: `[审计] 完全授权下发送了敏感类别: ${decision.sensitive.map((f) => f.kind).join(', ')} (只记类别, 不记明文) · grantId=${auth?.grantId} · goal=${opts.goalId || '-'}`,
335
+ });
336
+ }
337
+ const ev = await this.ledger({
338
+ activity: 'contact.sent', contactId: contact.contactId, goalId: opts.goalId, runId: opts.runId,
339
+ detail: `${deliver.reallySent ? '已真实外发' : '本地落盘(未真实外发)'} 通道 ${contact.provider} · 收件人 ${contact.displayValue} · ${summary}`
340
+ + ` · authorizationMode=${auth?.mode || 'one_time'}`
341
+ + ` · grantId=${auth?.grantId || '-'} grantVersion=${auth?.grantVersion ?? '-'}`
342
+ + ` · approvalSkipped=${auth?.approvalSkipped === true}`
343
+ + ` · policyDecision=${auth?.policyDecision || '-'}`
344
+ + (opts.approvedConsentId ? ` · 批准 ${opts.approvedConsentId}` : ''),
345
+ });
346
+ await this.ledger({
347
+ activity: 'contact.delivery_confirmed', contactId: contact.contactId, goalId: opts.goalId, runId: opts.runId,
348
+ detail: `${deliver.reallySent ? '通道确认投递' : '本地落盘确认 (非真实投递)'} id=${deliver.providerMessageId || '-'}`,
349
+ });
350
+ // 等待回复 → 绑定外部等待 + Goal 进 awaiting_external (发送成功但没回复 ≠ 目标完成)
351
+ if (opts.replyExpected && opts.goalId) {
352
+ const expiresAt = new Date(this.now() + (opts.replyWindowMs ?? 48 * 60 * 60_000)).toISOString();
353
+ const wait = {
354
+ requestId: opts.requestId,
355
+ continuationId: (0, external_events_js_1.newContinuationId)(opts.goalId),
356
+ expectedSource: 'contact',
357
+ expectedEvent: 'reply',
358
+ createdAt: new Date(this.now()).toISOString(),
359
+ expiresAt,
360
+ note: `等 ${contact.displayValue} 回复 (通道 ${contact.provider})`,
361
+ };
362
+ await (0, external_events_js_1.bindExternalWait)(opts.goalId, wait);
363
+ await (0, goal_store_js_1.updateGoal)(opts.goalId, { status: 'awaiting_external' });
364
+ const g = await (0, goal_store_js_1.readGoal)(opts.goalId);
365
+ await this.ledger({
366
+ activity: 'contact.authorization_requested', contactId: contact.contactId, goalId: opts.goalId, runId: opts.runId,
367
+ detail: `等待回复绑定成功 (expiresAt=${expiresAt})`,
368
+ evidenceRef: undefined,
369
+ }).catch(() => null);
370
+ return { status: 'awaiting_reply', requestId: opts.requestId, evidenceRef: ev.evidenceRef, goalStatus: g?.status };
371
+ }
372
+ return { status: 'sent', requestId: opts.requestId, evidenceRef: ev.evidenceRef, goalStatus: undefined };
373
+ }
374
+ /** 批准 → 执行 (把 body 从预留记录里带回来) */
375
+ async approveAndSend(consentId, opts = {}) {
376
+ const c = await this.consents.get(consentId);
377
+ if (!c)
378
+ return { ok: false, error: 'consent_not_found' };
379
+ const pending = await this.store.getPendingBody(c.requestId);
380
+ const body = opts.body ?? pending?.body ?? '';
381
+ if (!body.trim()) {
382
+ // 正文找不回来就明确失败 —— 绝不发一封空邮件出去
383
+ await this.consents.reject(consentId, { by: opts.by || 'system', reason: 'body_missing: 待批准正文丢失, 拒绝发送' });
384
+ return { ok: false, error: 'body_missing: 待批准正文丢失 (pending 文件不存在), 已拒绝发送', requestId: c.requestId };
385
+ }
386
+ (0, consent_js_1.setConsentExecutor)(async (cons) => {
387
+ const r = await this.performSend({
388
+ contactId: cons.contactId, goalId: cons.goalId, runId: cons.runId, taskKind: cons.taskRef,
389
+ subject: cons.subject, body, requestId: cons.requestId,
390
+ // 用待批准时记下的执行参数 (不是硬编码 true / 默认 48h)
391
+ replyExpected: pending?.replyExpected !== false,
392
+ replyWindowMs: pending?.replyWindowMs,
393
+ approvedConsentId: cons.consentId,
394
+ }, body);
395
+ return { ok: r.status === 'sent' || r.status === 'awaiting_reply', result: JSON.stringify({ status: r.status, evidenceRef: r.evidenceRef }), error: r.reason };
396
+ });
397
+ const r = await this.consents.approve(consentId, { by: opts.by, via: opts.via, now: this.now() });
398
+ await this.store.clearPendingBody(c.requestId); // 正文使命完成, 不留在盘上
399
+ if (!r.ok)
400
+ return { ok: false, error: r.error, requestId: c.requestId };
401
+ await this.ledger({
402
+ activity: 'contact.approved', contactId: c.contactId, goalId: c.goalId, runId: c.runId,
403
+ detail: `人工批准 (${opts.by || 'user'}, via ${opts.via || 'cli'}) · consent=${consentId}`,
404
+ });
405
+ // 批准 = 用户把这条联系方式授权给了这个任务
406
+ if (c.goalId)
407
+ await this.authorizeForTask({ contactId: c.contactId, goalId: c.goalId, by: opts.by || 'user' }).catch(() => null);
408
+ return { ok: true, status: r.consent?.status, requestId: c.requestId };
409
+ }
410
+ // ── 回复 (Phase 7) ────────────────────────────────────────────────────────
411
+ async reply(input) {
412
+ const rec = input.threadToken ? await this.store.findSendByThread(input.threadToken) : (input.requestId ? await this.store.findSend(input.requestId) : null);
413
+ if (!rec)
414
+ return { ok: false, reason: 'no_matching_send' };
415
+ const contact = await this.store.getContact(rec.contactId);
416
+ if (!contact)
417
+ return { ok: false, reason: 'contact_not_found' };
418
+ // 来源校验: 发信地址必须与绑定的联系方式一致 (否则不算证据、不唤醒)
419
+ const normFrom = contact.kind === 'phone' ? (0, types_js_1.normalizePhone)(input.from, 'CN') : (0, types_js_1.normalizeEmail)(input.from);
420
+ const same = normFrom.ok && normFrom.value === contact.normalizedValue;
421
+ if (!same) {
422
+ const ev = await this.ledger({
423
+ activity: 'contact.reply_untrusted', contactId: contact.contactId, goalId: rec.goalId, runId: rec.runId,
424
+ detail: `回复来源不可信 (不等于绑定的 ${contact.displayValue}) → 不计入证据、不唤醒`,
425
+ });
426
+ return { ok: false, reason: 'source_untrusted', evidenceRef: ev.evidenceRef };
427
+ }
428
+ const eventId = input.eventId || `cev-${this.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
429
+ const ev = await this.ledger({
430
+ activity: 'contact.reply_received', contactId: contact.contactId, goalId: rec.goalId, runId: rec.runId, eventId,
431
+ detail: `收到回复 (通道 ${contact.provider}) · ${(0, types_js_1.redactForEvidence)(input.body).replace(/\s+/g, ' ').slice(0, 120)}`,
432
+ });
433
+ await this.store.patchSend(rec.requestId, { status: 'delivery_confirmed' });
434
+ // 只唤醒**对应的那个** Goal (correlation 由 external-events 负责)
435
+ const delivered = await (0, external_events_js_1.deliverExternalEvent)({ source: 'contact', eventId, requestId: rec.requestId, goalId: rec.goalId, eventName: 'reply', payload: { body: (0, types_js_1.redactForEvidence)(input.body).slice(0, 300), contactId: contact.contactId, from: contact.displayValue } }, { wake: this.deps.wake, now: this.deps.now });
436
+ return { ok: delivered.ok, reason: delivered.reason, woke: delivered.woke, goalId: delivered.goalId || rec.goalId, evidenceRef: ev.evidenceRef };
437
+ }
438
+ /** 等待超时: 交给 external-events 判过期 → Goal 转 needs_human (人工接手) */
439
+ async expireWaits() {
440
+ const expired = await (0, external_events_js_1.expireExternalWaits)({ now: this.now() });
441
+ const out = [];
442
+ for (const e of expired) {
443
+ const g = await (0, goal_store_js_1.readGoal)(e.goalId);
444
+ const ev = await this.ledger({
445
+ activity: 'contact.wait_expired', goalId: e.goalId,
446
+ detail: `等待回复超时 (requestId=${e.wait.requestId}, 过期于 ${e.wait.expiresAt}) → 转人工`,
447
+ });
448
+ await (0, goal_store_js_1.setUnresolved)(e.goalId, Array.from(new Set([...(g?.unresolvedItems || []), `等待联系回复超时: ${e.wait.note || e.wait.requestId}`])));
449
+ out.push({ goalId: e.goalId, reason: e.reason, evidenceRef: ev.evidenceRef });
450
+ }
451
+ return out;
452
+ }
453
+ // ── 持久能力授权 (Phase 1/3/8/10) ──────────────────────────────────────────
454
+ /**
455
+ * 用户一次性给出长期授权 —— 之后 Agent 可以直接用, 不再逐次打断。
456
+ * 三个用户可见选项: '仅本次任务' / '长期使用'(推荐默认) / '完全授权联系方式能力'。
457
+ * **注意**: 授权不等于绕过安全边界 —— 单收件人/频率/任务关联/幂等/provider/证据/撤销/禁止群发 一个都不少。
458
+ */
459
+ async authorize(opts) {
460
+ const id = await this.ensureIdentity();
461
+ const g = await this.grants.create({ identityId: id.identityId, ownerDid: this.deps.ownerDid, ...opts });
462
+ const label = g.level === 'full_contact_access' ? '完全授权联系方式能力' : g.level === 'persistent' ? '长期自动使用' : '仅本次任务';
463
+ await this.ledger({
464
+ activity: 'contact.grant_created',
465
+ detail: `长期授权已建立: ${label} · channels=${g.channels} · contactScope=${g.contactScope} · taskScope=${g.taskScope} · contentScope=${g.contentScope} · grantId=${g.grantId} v${g.grantVersion} (by ${opts.grantedBy} via ${opts.grantedVia})`,
466
+ });
467
+ return { ok: true, grant: g, userLabel: label };
468
+ }
469
+ async pauseGrant(grantId, opts = {}) {
470
+ const r = await this.grants.transition(grantId, 'pause', opts);
471
+ if (r.ok && r.grant) {
472
+ await this.ledger({ activity: 'contact.grant_paused', detail: `授权已暂停 ${grantId} v${r.grant.grantVersion} (by ${opts.by || 'user'}) —— 新发送会被拦, 配置保留` });
473
+ }
474
+ return r;
475
+ }
476
+ async resumeGrant(grantId, opts = {}) {
477
+ const r = await this.grants.transition(grantId, 'resume', opts);
478
+ if (r.ok && r.grant)
479
+ await this.ledger({ activity: 'contact.grant_resumed', detail: `授权已恢复 ${grantId} v${r.grant.grantVersion}` });
480
+ return r;
481
+ }
482
+ /** 撤销 Grant: 立即阻止新发送 + 该授权下正在等待回复的任务清等待并转人工 */
483
+ async revokeGrant(grantId, opts = {}) {
484
+ const r = await this.grants.transition(grantId, 'revoke', opts);
485
+ if (!r.ok || !r.grant)
486
+ return { ok: false, error: r.error, affectedGoals: [] };
487
+ await this.ledger({ activity: 'contact.grant_revoked', detail: `授权已撤销 ${grantId} v${r.grant.grantVersion} (by ${opts.by || 'user'}${opts.reason ? `, ${opts.reason}` : ''}) —— 历史证据保留, 后续一律拒绝` });
488
+ const affected = [];
489
+ for (const s of await this.store.listSends()) {
490
+ if (s.grantId !== grantId || s.status !== 'sent' || !s.goalId)
491
+ continue;
492
+ await (0, external_events_js_1.clearExternalWait)(s.goalId);
493
+ const g = await (0, goal_store_js_1.readGoal)(s.goalId);
494
+ await (0, goal_store_js_1.setUnresolved)(s.goalId, Array.from(new Set([...(g?.unresolvedItems || []), `联系方式授权 ${grantId} 已撤销, 该联系不可自动重试 (转人工)`])));
495
+ // 撤等待 + 转人工: 不再自动唤醒, 由人接手 (Supervisor 下一轮看到 needs_human)
496
+ await (0, goal_store_js_1.setContinuation)(s.goalId, { wakeReason: 'needs_human', needsExternal: undefined, autoContinue: false }).catch(() => null);
497
+ await (0, goal_store_js_1.updateGoal)(s.goalId, { status: 'needs_human' }).catch(() => null);
498
+ affected.push(s.goalId);
499
+ }
500
+ return { ok: true, affectedGoals: affected };
501
+ }
502
+ /** 撤销全部联系方式授权 (等同"完全收回") */
503
+ async revokeAllGrants(opts = {}) {
504
+ const revoked = await this.grants.revokeAll(opts.by || 'user', opts.reason);
505
+ for (const g of revoked)
506
+ await this.ledger({ activity: 'contact.grant_revoked', detail: `授权已撤销 ${g.grantId} (by ${opts.by || 'user'}${opts.reason ? `, ${opts.reason}` : ''})` });
507
+ const affected = new Set();
508
+ for (const g of revoked) {
509
+ const r = await this.revokeGrant(g.grantId, opts).catch(() => ({ affectedGoals: [] }));
510
+ for (const a of r.affectedGoals || [])
511
+ affected.add(a);
512
+ }
513
+ return { revoked: revoked.map((g) => g.grantId), affectedGoals: [...affected] };
514
+ }
515
+ // ── 手机 → 桌面: 签名同步 (Phase 4) ────────────────────────────────────────
516
+ /** 登记手机设备公钥 (配对时由手机提供; 之后再接受它签名的授权) */
517
+ async registerDevice(deviceId, publicKeyPem, label) {
518
+ const d = await this.grants.registerDevice(deviceId, publicKeyPem, label);
519
+ await this.ledger({ activity: 'contact.grant_synced', detail: `登记设备公钥 ${deviceId}${label ? ` (${label})` : ''} —— 之后只接受该设备签名的授权` });
520
+ return d;
521
+ }
522
+ /** 接受手机同步来的签名 Grant (验签 + 版本冲突 + 撤销优先) */
523
+ async syncGrant(incoming) {
524
+ const r = await this.grants.applySignedSync(incoming);
525
+ if (r.ok) {
526
+ await this.ledger({ activity: 'contact.grant_synced', detail: `接受手机同步授权 ${incoming.grantId} v${incoming.grantVersion} (device=${incoming.signature?.deviceId || '-'}, level=${incoming.level})` });
527
+ }
528
+ else {
529
+ await this.ledger({ activity: 'contact.grant_sync_rejected', detail: `拒绝同步授权 ${incoming.grantId || '-'}: ${r.code} — ${r.reason}` });
530
+ }
531
+ return { ok: r.ok, reason: r.reason, code: r.code, grant: r.grant };
532
+ }
533
+ /** 手机撤销 → 桌面立即失效 */
534
+ async syncRevocation(grantId, opts = {}) {
535
+ const r = await this.grants.applyRevocation(grantId, opts);
536
+ if (r.ok) {
537
+ await this.ledger({ activity: 'contact.grant_revoked', detail: `收到手机端撤销 ${grantId} v${r.grant?.grantVersion} (by ${opts.by || 'mobile'}${opts.signature ? `, 签名设备 ${opts.signature.deviceId}` : ''})` });
538
+ }
539
+ else {
540
+ await this.ledger({ activity: 'contact.grant_sync_rejected', detail: `拒绝手机端撤销 ${grantId}: ${r.code} — ${r.reason}` });
541
+ }
542
+ return r;
543
+ }
544
+ /** 授权状态摘要 (CLI / Web / 手机读同一份) */
545
+ async grantsSummary() {
546
+ await this.ensureIdentity();
547
+ return this.grants.summary(this.deps.ownerDid);
548
+ }
549
+ // ── 迁移 (Phase 9) ────────────────────────────────────────────────────────
550
+ /**
551
+ * 老数据迁移规则 (刻意**保守**):
552
+ * - 没有长期 Grant 的已验证联系方式: 保持原行为 (每次仍需批准), 不自动升级
553
+ * - 已完成过人工批准: **不**推断为长期授权
554
+ * - 老 taskRefs: 只作为"任务级绑定"保留, 不扩成所有未来任务
555
+ * - requireApprovalEachTime=true: 保持最高优先级
556
+ * - 已撤销的联系方式: 绝不迁移成 active Grant
557
+ */
558
+ async migrateLegacy() {
559
+ const contacts = await this.store.listContacts();
560
+ const grantsBefore = (await this.grants.list()).length;
561
+ const keptOneTime = [];
562
+ const revokedSkipped = [];
563
+ const requireEachTimeKept = [];
564
+ for (const c of contacts) {
565
+ if (c.revokedAt || c.verificationStatus === 'revoked') {
566
+ revokedSkipped.push(c.contactId);
567
+ continue;
568
+ }
569
+ if ((c.consentScope?.taskRefs || []).length > 0 || c.verificationStatus === 'verified')
570
+ keptOneTime.push(c.contactId);
571
+ if (c.limits?.requireApprovalEachTime)
572
+ requireEachTimeKept.push(c.contactId);
573
+ }
574
+ return { scanned: contacts.length, keptOneTime, revokedSkipped, requireEachTimeKept, grantsBefore };
575
+ }
576
+ // ── 撤销 (Phase 1/6) ──────────────────────────────────────────────────────
577
+ async revoke(opts) {
578
+ const c = await this.store.getContact(opts.contactId);
579
+ if (!c)
580
+ return { ok: false, error: 'contact_not_found' };
581
+ c.revokedAt = new Date(this.now()).toISOString();
582
+ c.verificationStatus = 'revoked';
583
+ c.capabilities = [];
584
+ await this.store.putContact(c);
585
+ const ev = await this.ledger({
586
+ activity: 'contact.revoked', contactId: c.contactId,
587
+ detail: `授权撤销 (by ${opts.by || 'user'}${opts.reason ? `, ${opts.reason}` : ''}) —— 历史证据保留, 后续调用一律拒绝`,
588
+ });
589
+ // 撤掉的联系人还在等待回复的任务 → 清等待 + 记 unresolved (不允许重试, 也不许当完成)
590
+ const affected = [];
591
+ for (const s of await this.store.listSends()) {
592
+ if (s.contactId !== c.contactId || s.status !== 'sent' || !s.goalId)
593
+ continue;
594
+ await (0, external_events_js_1.clearExternalWait)(s.goalId);
595
+ const g = await (0, goal_store_js_1.readGoal)(s.goalId);
596
+ await (0, goal_store_js_1.setUnresolved)(s.goalId, Array.from(new Set([...(g?.unresolvedItems || []), `联系人 ${c.displayValue} 已撤销授权, 该联系不可重试`])));
597
+ await (0, goal_store_js_1.setContinuation)(s.goalId, { wakeReason: 'needs_human', needsExternal: undefined, autoContinue: false }).catch(() => null);
598
+ await (0, goal_store_js_1.updateGoal)(s.goalId, { status: 'needs_human' }).catch(() => null);
599
+ affected.push(s.goalId);
600
+ }
601
+ return { ok: true, evidenceRef: ev.evidenceRef, affectedGoals: affected };
602
+ }
603
+ // ── 查询 (只给脱敏视图) ────────────────────────────────────────────────────
604
+ async listAuthorized() {
605
+ await this.ensureIdentity();
606
+ const all = await this.store.listContacts();
607
+ const { externalStatusOf } = await Promise.resolve().then(() => __importStar(require('./types.js')));
608
+ return all.map((c) => ({
609
+ contactId: c.contactId, kind: c.kind, displayValue: c.displayValue,
610
+ status: externalStatusOf(c), provider: c.provider, capabilities: c.capabilities,
611
+ policy: c.policy, aliases: c.aliases || [],
612
+ }));
613
+ }
614
+ channels() { return (0, providers_js_1.listProviders)(); }
615
+ }
616
+ exports.ContactChain = ContactChain;
617
+ /** 便捷: 一次构造 */
618
+ function contactChain(deps) { return new ContactChain(deps); }
619
+ //# sourceMappingURL=chain.js.map