evolcore 0.0.3 → 0.0.4
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/CHANGELOG.md +64 -0
- package/README.md +45 -10
- package/bin/ec-safe-output.js +89 -24
- package/dist/agents/baseagent.js +46 -0
- package/dist/agents/claude-runner.js +1 -31
- package/dist/agents/codex-app-server-client.js +68 -0
- package/dist/agents/codex-runner.js +157 -5
- package/dist/agents/runner-types.js +2 -2
- package/dist/aun/aid/agentmd.js +79 -0
- package/dist/aun/aid/encryption-seed-policy.js +29 -0
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/aid/store.js +2 -5
- package/dist/channels/aun.js +220 -112
- package/dist/channels/daemon.js +18 -4
- package/dist/channels/dingtalk.js +52 -137
- package/dist/channels/feishu.js +56 -4
- package/dist/channels/qqbot.js +23 -1
- package/dist/channels/wechat.js +303 -155
- package/dist/channels/wecom.js +383 -7
- package/dist/cli/aun-commands.js +11 -11
- package/dist/cli/handoff-command.js +2 -1
- package/dist/cli/init-channel.js +185 -67
- package/dist/cli/init.js +7 -5
- package/dist/cli/trigger-command.js +92 -11
- package/dist/config/access-policy-domain.js +38 -0
- package/dist/config/access-policy.js +134 -0
- package/dist/config/builtin-role-templates.js +27 -14
- package/dist/config/builtin-roles.js +25 -6
- package/dist/config/config-field-policy.js +17 -5
- package/dist/config/config-manager.js +198 -22
- package/dist/config/config-operation-service.js +35 -38
- package/dist/config/contact-bind-code.js +274 -0
- package/dist/config/contact-book-store.js +173 -5
- package/dist/config/contact-book.js +32 -0
- package/dist/config/contact-operation-service.js +9 -3
- package/dist/config/contact-request-service.js +331 -0
- package/dist/config/peer-role-resolver.js +3 -1
- package/dist/config/schema-registry.js +49 -24
- package/dist/config-store.js +14 -2
- package/dist/core/auth/agent-delegation.js +105 -11
- package/dist/core/auth/authorization-audit.js +6 -0
- package/dist/core/auth/operation-authorizer.js +8 -0
- package/dist/core/auth/operation-catalog.js +51 -3
- package/dist/core/auth/trigger-authorization.js +15 -0
- package/dist/core/bootstrap-service.js +2 -9
- package/dist/core/channel-loader.js +6 -2
- package/dist/core/command/command-handler.js +10 -13
- package/dist/core/command/connect-menu.js +249 -24
- package/dist/core/command/evol-menu-version-gate.js +38 -0
- package/dist/core/command/group-menu.js +421 -0
- package/dist/core/command/menu-handler.js +269 -85
- package/dist/core/command/menu-protocol.js +8 -2
- package/dist/core/command/menu-token-store.js +102 -0
- package/dist/core/command/slash-gate.js +1 -1
- package/dist/core/command/slash-handler.js +158 -31
- package/dist/core/daemon-file-cache.js +28 -0
- package/dist/core/event-catalog.js +29 -0
- package/dist/core/evolagent-registry.js +4 -39
- package/dist/core/handoff/runtime.js +162 -37
- package/dist/core/handoff/store.js +46 -2
- package/dist/core/handoff/types.js +1 -0
- package/dist/core/inference/text-inference.js +16 -74
- package/dist/core/message/file-markers.js +7 -0
- package/dist/core/message/im-renderer.js +18 -14
- package/dist/core/message/inbound-admission.js +124 -0
- package/dist/core/message/message-bridge.js +616 -74
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +185 -10
- package/dist/core/message/peer-mode.js +7 -8
- package/dist/core/message/response-engine.js +381 -79
- package/dist/core/permission/approval-gateway.js +17 -10
- package/dist/core/permission/ec-command-parser.js +101 -46
- package/dist/core/permission/tool-policy.js +69 -24
- package/dist/core/protected-paths.js +36 -11
- package/dist/core/session/session-manager.js +25 -3
- package/dist/core/session/session-mapper.js +2 -0
- package/dist/core/session/session-renew.js +125 -69
- package/dist/core/session/session-turn-coordinator.js +5 -1
- package/dist/eck/kit-renderer.js +12 -1
- package/dist/eck/message-renderer.js +79 -1
- package/dist/index.js +224 -89
- package/dist/ipc.js +81 -4
- package/dist/paths.js +3 -0
- package/dist/response-system/config-resolver.js +29 -0
- package/dist/response-system/coordinator.js +8 -32
- package/dist/response-system/engines/v1/proactive-flow.js +1 -1
- package/dist/response-system/index.js +1 -0
- package/dist/response-system/modes/single-session/index.js +7 -4
- package/dist/trigger/parser.js +55 -15
- package/dist/trigger/patch.js +4 -1
- package/dist/trigger/scheduler.js +441 -39
- package/dist/utils/error-dict.json +7 -0
- package/dist/utils/evolcore-version.js +21 -0
- package/dist/utils/logger.js +2 -2
- package/dist/utils/stable-semver.js +21 -0
- package/dist/utils/stats.js +33 -9
- package/kits/docs/channels/aun.md +4 -13
- package/kits/docs/evolcore/INDEX.md +1 -1
- package/kits/docs/evolcore/config.md +47 -2
- package/kits/docs/evolcore/contact.md +7 -3
- package/kits/docs/evolcore/group-rules.md +46 -4
- package/kits/docs/evolcore/group.md +4 -4
- package/kits/docs/evolcore/msg.md +5 -5
- package/kits/docs/evolcore/trigger.md +25 -8
- package/kits/docs/path-registry.md +36 -17
- package/kits/eck_message_manifest.json +12 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +7 -0
- package/kits/rules/01-overview.md +17 -7
- package/kits/rules/02-navigation.md +38 -18
- package/kits/rules/03-identity.md +28 -24
- package/kits/rules/04-relation.md +44 -28
- package/kits/rules/05-venue.md +31 -15
- package/kits/rules/06-channel.md +11 -7
- package/kits/schemas/_meta.json +18 -7
- package/kits/schemas/agent-config.schema.7.json +303 -0
- package/kits/schemas/agent-config.schema.8.json +304 -0
- package/kits/schemas/agent-config.schema.9.json +374 -0
- package/kits/schemas/contact-book.schema.3.json +67 -0
- package/kits/schemas/daemon.schema.1.json +3 -2
- package/kits/schemas/daemon.schema.2.json +101 -0
- package/kits/schemas/daemon.schema.3.json +123 -0
- package/kits/schemas/defaults.schema.2.json +85 -0
- package/kits/schemas/defaults.schema.3.json +73 -0
- package/kits/schemas/relation-config.schema.6.json +46 -0
- package/kits/schemas/relation-config.schema.7.json +59 -0
- package/kits/schemas/single-session.schema.2.json +57 -0
- package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
- package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
- package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
- package/kits/templates/roles/admin.json +46 -0
- package/kits/templates/roles/member.json +4 -0
- package/kits/templates/roles/visitor.json +4 -0
- package/kits/templates/system-fragments/channel.md +12 -2
- package/kits/templates/system-fragments/identity.md +3 -1
- package/kits/templates/system-fragments/relation.md +1 -1
- package/kits/templates/system-fragments/session.md +6 -2
- package/package.json +4 -2
- package/MIGRATION-0.5.0.md +0 -378
- package/ROLE_ACCESS_CONTROL.md +0 -174
- package/dist/channels/contact-bind-code.js +0 -134
- package/dist/channels/wecom-card.js +0 -101
- package/dist/channels/wecom-onboarding.js +0 -82
- package/dist/channels/wecom-state.js +0 -191
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { randomInt } from 'crypto';
|
|
2
|
-
import { isValidAid } from '../aun/aid/validation.js';
|
|
3
|
-
import { bindContactAlias } from '../config/contact-book.js';
|
|
4
|
-
export const CONTACT_BIND_CODE_TTL_MS = 10 * 60 * 1000;
|
|
5
|
-
export const CONTACT_BIND_MAX_FAILED_ATTEMPTS = 5;
|
|
6
|
-
/** In-memory, one-time channel identity proof used before writing a Contact alias. */
|
|
7
|
-
export class ContactBindCodeRegistry {
|
|
8
|
-
options;
|
|
9
|
-
pending = new Map();
|
|
10
|
-
constructor(options) {
|
|
11
|
-
this.options = options;
|
|
12
|
-
}
|
|
13
|
-
register(req) {
|
|
14
|
-
const selfAid = String(req.selfAid || '').trim();
|
|
15
|
-
const channelName = String(req.channelName || '').trim();
|
|
16
|
-
const primaryId = String(req.primaryId || '').trim();
|
|
17
|
-
const now = req.now ?? Date.now();
|
|
18
|
-
const code = req.code ? String(req.code).trim() : generateBindCode();
|
|
19
|
-
if (!isValidAid(selfAid))
|
|
20
|
-
return { ok: false, error: `invalid selfAid: ${req.selfAid}` };
|
|
21
|
-
if (!channelName)
|
|
22
|
-
return { ok: false, error: 'missing channelName' };
|
|
23
|
-
if (!isValidAid(primaryId))
|
|
24
|
-
return { ok: false, error: `invalid primaryId: ${req.primaryId}` };
|
|
25
|
-
if (!/^\d{6}$/.test(code))
|
|
26
|
-
return { ok: false, error: 'binding code must be 6 digits' };
|
|
27
|
-
const key = pendingBindKey(selfAid, channelName);
|
|
28
|
-
const replaced = this.pending.has(key);
|
|
29
|
-
const item = {
|
|
30
|
-
selfAid,
|
|
31
|
-
channelName,
|
|
32
|
-
primaryId,
|
|
33
|
-
code,
|
|
34
|
-
createdAt: now,
|
|
35
|
-
expiresAt: now + CONTACT_BIND_CODE_TTL_MS,
|
|
36
|
-
failedAttempts: 0,
|
|
37
|
-
maxFailedAttempts: CONTACT_BIND_MAX_FAILED_ATTEMPTS,
|
|
38
|
-
};
|
|
39
|
-
this.pending.set(key, item);
|
|
40
|
-
return { ok: true, code, expiresAt: item.expiresAt, replaced };
|
|
41
|
-
}
|
|
42
|
-
handle(ctx) {
|
|
43
|
-
if (ctx.channelType !== this.options.channelType)
|
|
44
|
-
return { handled: false };
|
|
45
|
-
const selfAid = String(ctx.selfAid || '').trim();
|
|
46
|
-
const channelName = String(ctx.channelName || '').trim();
|
|
47
|
-
if (!selfAid || !channelName)
|
|
48
|
-
return { handled: false };
|
|
49
|
-
const key = pendingBindKey(selfAid, channelName);
|
|
50
|
-
const item = this.pending.get(key);
|
|
51
|
-
if (!item)
|
|
52
|
-
return { handled: false };
|
|
53
|
-
const now = ctx.now ?? Date.now();
|
|
54
|
-
if (now >= item.expiresAt) {
|
|
55
|
-
this.pending.delete(key);
|
|
56
|
-
return {
|
|
57
|
-
handled: true,
|
|
58
|
-
status: 'expired',
|
|
59
|
-
remainingAttempts: 0,
|
|
60
|
-
reply: `${this.options.displayName}身份绑定码已超时,本次绑定失败。请重新执行 ${this.options.initCommand} 并再次绑定。`,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
if (ctx.chatType !== 'private')
|
|
64
|
-
return { handled: false };
|
|
65
|
-
const input = String(ctx.content ?? '').trim();
|
|
66
|
-
if (!/^\d{6}$/.test(input)) {
|
|
67
|
-
return {
|
|
68
|
-
handled: true,
|
|
69
|
-
status: 'format',
|
|
70
|
-
reply: '请直接发送 6 位数字绑定码。',
|
|
71
|
-
remainingAttempts: item.maxFailedAttempts - item.failedAttempts,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
if (input !== item.code) {
|
|
75
|
-
item.failedAttempts += 1;
|
|
76
|
-
const remaining = Math.max(0, item.maxFailedAttempts - item.failedAttempts);
|
|
77
|
-
if (remaining === 0) {
|
|
78
|
-
this.pending.delete(key);
|
|
79
|
-
return {
|
|
80
|
-
handled: true,
|
|
81
|
-
status: 'failed',
|
|
82
|
-
remainingAttempts: 0,
|
|
83
|
-
reply: `绑定码无效,本次${this.options.displayName}身份绑定失败。请重新执行 ${this.options.initCommand} 并再次绑定。`,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
return {
|
|
87
|
-
handled: true,
|
|
88
|
-
status: 'wrong-code',
|
|
89
|
-
remainingAttempts: remaining,
|
|
90
|
-
reply: '绑定码错误,请重新发送 6 位数字绑定码。',
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
const actorId = String(ctx.actorId || '').trim();
|
|
94
|
-
if (!actorId) {
|
|
95
|
-
return {
|
|
96
|
-
handled: true,
|
|
97
|
-
status: 'missing-actor',
|
|
98
|
-
remainingAttempts: item.maxFailedAttempts - item.failedAttempts,
|
|
99
|
-
reply: `无法识别当前${this.options.displayName}发送者身份,未建立绑定。请重新发送绑定码或重新执行绑定流程。`,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
try {
|
|
103
|
-
bindContactAlias(item.selfAid, item.primaryId, this.options.channelType, actorId, item.channelName);
|
|
104
|
-
this.pending.delete(key);
|
|
105
|
-
return {
|
|
106
|
-
handled: true,
|
|
107
|
-
status: 'bound',
|
|
108
|
-
reply: `${this.options.displayName}身份绑定成功:${item.channelName}:${encodeURIComponent(actorId)} -> ${item.primaryId}`,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
catch (error) {
|
|
112
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
113
|
-
return {
|
|
114
|
-
handled: true,
|
|
115
|
-
status: 'write-failed',
|
|
116
|
-
remainingAttempts: item.maxFailedAttempts - item.failedAttempts,
|
|
117
|
-
reply: `${this.options.displayName}身份绑定写入失败:${message}`,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
get(selfAid, channelName) {
|
|
122
|
-
const item = this.pending.get(pendingBindKey(selfAid, channelName));
|
|
123
|
-
return item ? { ...item } : null;
|
|
124
|
-
}
|
|
125
|
-
clear() {
|
|
126
|
-
this.pending.clear();
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
function generateBindCode() {
|
|
130
|
-
return String(randomInt(0, 1_000_000)).padStart(6, '0');
|
|
131
|
-
}
|
|
132
|
-
function pendingBindKey(selfAid, channelName) {
|
|
133
|
-
return `${String(selfAid || '').trim()}\u0000${String(channelName || '').trim()}`;
|
|
134
|
-
}
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
function truncate(value, max) {
|
|
2
|
-
if (!value)
|
|
3
|
-
return undefined;
|
|
4
|
-
const chars = [...value.trim()];
|
|
5
|
-
return chars.length <= max ? value.trim() : `${chars.slice(0, Math.max(1, max - 1)).join('')}...`;
|
|
6
|
-
}
|
|
7
|
-
function buttonStyle(style) {
|
|
8
|
-
if (style === 'primary')
|
|
9
|
-
return 2;
|
|
10
|
-
if (style === 'danger')
|
|
11
|
-
return 3;
|
|
12
|
-
return 1;
|
|
13
|
-
}
|
|
14
|
-
export function buildWecomInteractionCard(interaction, taskId) {
|
|
15
|
-
const bodyParts = [];
|
|
16
|
-
if (interaction.kind.body)
|
|
17
|
-
bodyParts.push(interaction.kind.body);
|
|
18
|
-
if (interaction.kind.kind === 'action' && interaction.kind.checkers?.length) {
|
|
19
|
-
bodyParts.push(interaction.kind.checkers.map((item, index) => `${index + 1}. ${item.label}`).join('\n'));
|
|
20
|
-
}
|
|
21
|
-
return {
|
|
22
|
-
card_type: 'button_interaction',
|
|
23
|
-
task_id: taskId,
|
|
24
|
-
main_title: {
|
|
25
|
-
title: truncate(interaction.kind.title, 26),
|
|
26
|
-
desc: truncate(bodyParts.join('\n\n'), 120),
|
|
27
|
-
},
|
|
28
|
-
button_list: interaction.kind.buttons
|
|
29
|
-
.map((button, index) => ({ button, index }))
|
|
30
|
-
.filter(({ button }) => !('disabled' in button) || !button.disabled)
|
|
31
|
-
.slice(0, 6)
|
|
32
|
-
.map(({ button, index }) => ({
|
|
33
|
-
text: truncate(button.label, 10) || `选项 ${index + 1}`,
|
|
34
|
-
style: buttonStyle(button.style),
|
|
35
|
-
key: interaction.kind.kind === 'command-card' ? `cmd_${index}` : `act_${index}`,
|
|
36
|
-
})),
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
export function parseWecomCardResponse(record, event, operatorId) {
|
|
40
|
-
const eventKey = String(event.event_key ?? '').trim();
|
|
41
|
-
const match = /^(cmd|act)_(\d+)$/.exec(eventKey);
|
|
42
|
-
if (!match)
|
|
43
|
-
return null;
|
|
44
|
-
const index = Number(match[2]);
|
|
45
|
-
if (record.interaction.kind.kind === 'command-card') {
|
|
46
|
-
const button = record.interaction.kind.buttons[index];
|
|
47
|
-
if (!button || button.disabled)
|
|
48
|
-
return null;
|
|
49
|
-
return { command: button.command };
|
|
50
|
-
}
|
|
51
|
-
const button = record.interaction.kind.buttons[index];
|
|
52
|
-
if (!button)
|
|
53
|
-
return null;
|
|
54
|
-
const values = {};
|
|
55
|
-
const selectedItems = event.selected_items?.selected_item;
|
|
56
|
-
if (Array.isArray(selectedItems)) {
|
|
57
|
-
for (const selected of selectedItems) {
|
|
58
|
-
const questionKey = String(selected?.question_key ?? '').trim();
|
|
59
|
-
const optionIds = selected?.option_ids?.option_id;
|
|
60
|
-
if (questionKey && Array.isArray(optionIds))
|
|
61
|
-
values[questionKey] = optionIds.filter(Boolean);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return {
|
|
65
|
-
response: {
|
|
66
|
-
type: 'interaction.response',
|
|
67
|
-
id: record.interaction.id,
|
|
68
|
-
action: button.key,
|
|
69
|
-
values: Object.keys(values).length > 0 ? values : undefined,
|
|
70
|
-
operatorId,
|
|
71
|
-
},
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
export function buildWecomTerminalCard(record, status, detail) {
|
|
75
|
-
const statusText = status === 'resolved'
|
|
76
|
-
? `已处理${detail ? `:${detail}` : ''}`
|
|
77
|
-
: status === 'rejected'
|
|
78
|
-
? '仅卡片发起者可操作'
|
|
79
|
-
: status === 'unavailable'
|
|
80
|
-
? '处理服务暂不可用,请重新发起'
|
|
81
|
-
: `卡片已失效${detail ? `:${detail}` : ''}`;
|
|
82
|
-
return {
|
|
83
|
-
card_type: 'text_notice',
|
|
84
|
-
task_id: record.taskId,
|
|
85
|
-
main_title: {
|
|
86
|
-
title: truncate(record.interaction.kind.title, 26),
|
|
87
|
-
desc: truncate(statusText, 120),
|
|
88
|
-
},
|
|
89
|
-
sub_title_text: truncate(record.interaction.kind.body, 120),
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
export function buildWecomUnknownCard(taskId) {
|
|
93
|
-
return {
|
|
94
|
-
card_type: 'text_notice',
|
|
95
|
-
task_id: taskId,
|
|
96
|
-
main_title: {
|
|
97
|
-
title: '卡片已失效',
|
|
98
|
-
desc: '本地状态不存在,请重新发起操作',
|
|
99
|
-
},
|
|
100
|
-
};
|
|
101
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import os from 'node:os';
|
|
2
|
-
const WECOM_QR_GENERATE_URL = 'https://work.weixin.qq.com/ai/qc/generate';
|
|
3
|
-
const WECOM_QR_QUERY_URL = 'https://work.weixin.qq.com/ai/qc/query_result';
|
|
4
|
-
const WECOM_QR_PAGE_URL = 'https://work.weixin.qq.com/ai/qc/gen';
|
|
5
|
-
function platformCode() {
|
|
6
|
-
if (os.platform() === 'darwin')
|
|
7
|
-
return 1;
|
|
8
|
-
if (os.platform() === 'win32')
|
|
9
|
-
return 2;
|
|
10
|
-
if (os.platform() === 'linux')
|
|
11
|
-
return 3;
|
|
12
|
-
return 0;
|
|
13
|
-
}
|
|
14
|
-
async function fetchJson(url, fetchImpl, signal) {
|
|
15
|
-
const timeoutSignal = AbortSignal.timeout(15_000);
|
|
16
|
-
const response = await fetchImpl(url, {
|
|
17
|
-
headers: { Accept: 'application/json' },
|
|
18
|
-
signal: signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal,
|
|
19
|
-
});
|
|
20
|
-
if (!response.ok)
|
|
21
|
-
throw new Error(`企微扫码服务请求失败: HTTP ${response.status}`);
|
|
22
|
-
return response.json();
|
|
23
|
-
}
|
|
24
|
-
export async function createWecomQrSession(fetchImpl = fetch) {
|
|
25
|
-
const url = new URL(WECOM_QR_GENERATE_URL);
|
|
26
|
-
url.searchParams.set('source', 'wecom-cli');
|
|
27
|
-
url.searchParams.set('plat', String(platformCode()));
|
|
28
|
-
const payload = await fetchJson(url, fetchImpl);
|
|
29
|
-
const scode = String(payload?.data?.scode ?? '').trim();
|
|
30
|
-
const authUrl = String(payload?.data?.auth_url ?? '').trim();
|
|
31
|
-
if (!scode || !authUrl)
|
|
32
|
-
throw new Error('企微扫码服务未返回 scode 或授权地址');
|
|
33
|
-
const browserUrl = new URL(WECOM_QR_PAGE_URL);
|
|
34
|
-
browserUrl.searchParams.set('source', 'wecom-cli');
|
|
35
|
-
browserUrl.searchParams.set('scode', scode);
|
|
36
|
-
return { scode, authUrl, browserUrl: browserUrl.toString() };
|
|
37
|
-
}
|
|
38
|
-
export async function pollWecomQrCredentials(scode, opts) {
|
|
39
|
-
const fetchImpl = opts?.fetchImpl ?? fetch;
|
|
40
|
-
const timeoutMs = opts?.timeoutMs ?? 5 * 60 * 1000;
|
|
41
|
-
const intervalMs = opts?.intervalMs ?? 3_000;
|
|
42
|
-
const deadline = Date.now() + timeoutMs;
|
|
43
|
-
const url = new URL(WECOM_QR_QUERY_URL);
|
|
44
|
-
url.searchParams.set('scode', scode);
|
|
45
|
-
let previousStatus = '';
|
|
46
|
-
while (Date.now() < deadline) {
|
|
47
|
-
if (opts?.signal?.aborted)
|
|
48
|
-
throw new Error('企微扫码已取消');
|
|
49
|
-
const payload = await fetchJson(url, fetchImpl, opts?.signal);
|
|
50
|
-
const status = String(payload?.data?.status ?? 'waiting');
|
|
51
|
-
if (status !== previousStatus) {
|
|
52
|
-
opts?.onStatus?.(status);
|
|
53
|
-
previousStatus = status;
|
|
54
|
-
}
|
|
55
|
-
if (status === 'success') {
|
|
56
|
-
const botId = String(payload?.data?.bot_info?.botid ?? '').trim();
|
|
57
|
-
const secret = String(payload?.data?.bot_info?.secret ?? '').trim();
|
|
58
|
-
if (!botId || !secret)
|
|
59
|
-
throw new Error('扫码成功,但企微未返回 Bot ID 或 Secret');
|
|
60
|
-
return { botId, secret };
|
|
61
|
-
}
|
|
62
|
-
if (status === 'expired' || status === 'cancelled' || status === 'rejected') {
|
|
63
|
-
throw new Error(`企微扫码未完成: ${status}`);
|
|
64
|
-
}
|
|
65
|
-
await new Promise((resolve, reject) => {
|
|
66
|
-
const finish = () => {
|
|
67
|
-
opts?.signal?.removeEventListener('abort', onAbort);
|
|
68
|
-
resolve();
|
|
69
|
-
};
|
|
70
|
-
const timer = setTimeout(finish, intervalMs);
|
|
71
|
-
const onAbort = () => {
|
|
72
|
-
clearTimeout(timer);
|
|
73
|
-
opts?.signal?.removeEventListener('abort', onAbort);
|
|
74
|
-
reject(new Error('企微扫码已取消'));
|
|
75
|
-
};
|
|
76
|
-
opts?.signal?.addEventListener('abort', onAbort, { once: true });
|
|
77
|
-
if (opts?.signal?.aborted)
|
|
78
|
-
onAbort();
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
throw new Error('企微扫码超时,请重新生成二维码');
|
|
82
|
-
}
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import crypto from 'node:crypto';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { agentDataDir } from '../paths.js';
|
|
5
|
-
import { atomicReadJson, atomicWriteJson } from '../utils/atomic-write.js';
|
|
6
|
-
import { logger } from '../utils/logger.js';
|
|
7
|
-
const STORE_VERSION = 1;
|
|
8
|
-
const DEFAULT_DEDUP_TTL_MS = 24 * 60 * 60 * 1000;
|
|
9
|
-
const DEFAULT_CARD_RETENTION_MS = 24 * 60 * 60 * 1000;
|
|
10
|
-
function writePrivateJson(filePath, value) {
|
|
11
|
-
atomicWriteJson(filePath, value);
|
|
12
|
-
try {
|
|
13
|
-
fs.chmodSync(filePath, 0o600);
|
|
14
|
-
}
|
|
15
|
-
catch { /* best effort on non-POSIX filesystems */ }
|
|
16
|
-
}
|
|
17
|
-
export class PersistentWecomDeduper {
|
|
18
|
-
filePath;
|
|
19
|
-
channelKey;
|
|
20
|
-
entries = new Map();
|
|
21
|
-
ttlMs;
|
|
22
|
-
maxEntries;
|
|
23
|
-
constructor(filePath, channelKey, opts) {
|
|
24
|
-
this.filePath = filePath;
|
|
25
|
-
this.channelKey = channelKey;
|
|
26
|
-
this.ttlMs = opts?.ttlMs ?? DEFAULT_DEDUP_TTL_MS;
|
|
27
|
-
this.maxEntries = opts?.maxEntries ?? 20_000;
|
|
28
|
-
this.load();
|
|
29
|
-
}
|
|
30
|
-
checkAndMark(messageId, now = Date.now()) {
|
|
31
|
-
this.evict(now);
|
|
32
|
-
const seenAt = this.entries.get(messageId);
|
|
33
|
-
if (seenAt !== undefined && now - seenAt <= this.ttlMs)
|
|
34
|
-
return true;
|
|
35
|
-
this.entries.set(messageId, now);
|
|
36
|
-
this.trimToLimit();
|
|
37
|
-
this.flush();
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
get size() {
|
|
41
|
-
return this.entries.size;
|
|
42
|
-
}
|
|
43
|
-
load() {
|
|
44
|
-
if (!this.filePath)
|
|
45
|
-
return;
|
|
46
|
-
try {
|
|
47
|
-
const stored = atomicReadJson(this.filePath);
|
|
48
|
-
if (!stored || stored.version !== STORE_VERSION || stored.channelKey !== this.channelKey)
|
|
49
|
-
return;
|
|
50
|
-
for (const [id, timestamp] of Object.entries(stored.entries ?? {})) {
|
|
51
|
-
if (id && Number.isFinite(timestamp))
|
|
52
|
-
this.entries.set(id, timestamp);
|
|
53
|
-
}
|
|
54
|
-
const before = this.entries.size;
|
|
55
|
-
this.evict(Date.now());
|
|
56
|
-
this.trimToLimit();
|
|
57
|
-
if (this.entries.size !== before)
|
|
58
|
-
this.flush();
|
|
59
|
-
}
|
|
60
|
-
catch (error) {
|
|
61
|
-
logger.warn(`[WeCom] Ignoring unreadable dedup store for ${this.channelKey}: ${String(error)}`);
|
|
62
|
-
this.entries.clear();
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
evict(now) {
|
|
66
|
-
for (const [id, timestamp] of this.entries) {
|
|
67
|
-
if (now - timestamp > this.ttlMs)
|
|
68
|
-
this.entries.delete(id);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
trimToLimit() {
|
|
72
|
-
if (this.entries.size <= this.maxEntries)
|
|
73
|
-
return;
|
|
74
|
-
const oldest = [...this.entries.entries()].sort((a, b) => a[1] - b[1]);
|
|
75
|
-
for (let i = 0; i < oldest.length - this.maxEntries; i++) {
|
|
76
|
-
this.entries.delete(oldest[i][0]);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
flush() {
|
|
80
|
-
writePrivateJson(this.filePath, {
|
|
81
|
-
version: STORE_VERSION,
|
|
82
|
-
channelKey: this.channelKey,
|
|
83
|
-
entries: Object.fromEntries(this.entries),
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
export class PersistentWecomCardStore {
|
|
88
|
-
filePath;
|
|
89
|
-
channelKey;
|
|
90
|
-
records = new Map();
|
|
91
|
-
constructor(filePath, channelKey) {
|
|
92
|
-
this.filePath = filePath;
|
|
93
|
-
this.channelKey = channelKey;
|
|
94
|
-
if (filePath)
|
|
95
|
-
this.load();
|
|
96
|
-
}
|
|
97
|
-
set(record) {
|
|
98
|
-
this.records.set(record.taskId, record);
|
|
99
|
-
this.cleanup();
|
|
100
|
-
this.flush();
|
|
101
|
-
}
|
|
102
|
-
get(taskId) {
|
|
103
|
-
this.cleanup();
|
|
104
|
-
return this.records.get(taskId);
|
|
105
|
-
}
|
|
106
|
-
findByInteractionId(interactionId) {
|
|
107
|
-
this.cleanup();
|
|
108
|
-
return [...this.records.values()].find(record => record.interaction.id === interactionId);
|
|
109
|
-
}
|
|
110
|
-
listPendingByChat(chatId) {
|
|
111
|
-
this.cleanup();
|
|
112
|
-
return [...this.records.values()].filter(record => (record.chatId === chatId
|
|
113
|
-
&& record.status === 'pending'
|
|
114
|
-
&& (!record.expiresAt || record.expiresAt >= Date.now())));
|
|
115
|
-
}
|
|
116
|
-
resolve(taskId, action) {
|
|
117
|
-
const record = this.records.get(taskId);
|
|
118
|
-
if (!record)
|
|
119
|
-
return undefined;
|
|
120
|
-
record.status = 'resolved';
|
|
121
|
-
record.resolution = action;
|
|
122
|
-
record.updatedAt = Date.now();
|
|
123
|
-
this.flush();
|
|
124
|
-
return record;
|
|
125
|
-
}
|
|
126
|
-
invalidateByInteractionId(interactionId, reason) {
|
|
127
|
-
const record = this.findByInteractionId(interactionId);
|
|
128
|
-
if (!record || record.status === 'resolved')
|
|
129
|
-
return record;
|
|
130
|
-
record.status = 'invalidated';
|
|
131
|
-
record.invalidationReason = reason;
|
|
132
|
-
record.updatedAt = Date.now();
|
|
133
|
-
this.flush();
|
|
134
|
-
return record;
|
|
135
|
-
}
|
|
136
|
-
load() {
|
|
137
|
-
if (!this.filePath)
|
|
138
|
-
return;
|
|
139
|
-
try {
|
|
140
|
-
const stored = atomicReadJson(this.filePath);
|
|
141
|
-
if (!stored || stored.version !== STORE_VERSION || stored.channelKey !== this.channelKey)
|
|
142
|
-
return;
|
|
143
|
-
for (const record of stored.records ?? []) {
|
|
144
|
-
if (record?.taskId && record?.interaction?.id)
|
|
145
|
-
this.records.set(record.taskId, record);
|
|
146
|
-
}
|
|
147
|
-
this.cleanup(true);
|
|
148
|
-
}
|
|
149
|
-
catch (error) {
|
|
150
|
-
logger.warn(`[WeCom] Ignoring unreadable card store for ${this.channelKey}: ${String(error)}`);
|
|
151
|
-
this.records.clear();
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
cleanup(flushWhenChanged = false) {
|
|
155
|
-
const now = Date.now();
|
|
156
|
-
let changed = false;
|
|
157
|
-
for (const [taskId, record] of this.records) {
|
|
158
|
-
const terminalExpiry = record.updatedAt + DEFAULT_CARD_RETENTION_MS;
|
|
159
|
-
const pendingExpiry = (record.expiresAt ?? record.createdAt + 7 * DEFAULT_CARD_RETENTION_MS)
|
|
160
|
-
+ DEFAULT_CARD_RETENTION_MS;
|
|
161
|
-
const expiresAt = record.status === 'pending' ? pendingExpiry : terminalExpiry;
|
|
162
|
-
if (now > expiresAt) {
|
|
163
|
-
this.records.delete(taskId);
|
|
164
|
-
changed = true;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
if (changed && flushWhenChanged)
|
|
168
|
-
this.flush();
|
|
169
|
-
}
|
|
170
|
-
flush() {
|
|
171
|
-
if (!this.filePath)
|
|
172
|
-
return;
|
|
173
|
-
writePrivateJson(this.filePath, {
|
|
174
|
-
version: STORE_VERSION,
|
|
175
|
-
channelKey: this.channelKey,
|
|
176
|
-
records: [...this.records.values()],
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
export function wecomChannelDataPaths(agentAid, channelKey) {
|
|
181
|
-
const safeKey = encodeURIComponent(channelKey);
|
|
182
|
-
const dir = path.join(agentDataDir(agentAid), 'channels', safeKey);
|
|
183
|
-
return {
|
|
184
|
-
dedupFile: path.join(dir, 'wecom-message-dedup.json'),
|
|
185
|
-
cardFile: path.join(dir, 'wecom-cards.json'),
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
export function createWecomCardTaskId(channelKey, interactionId) {
|
|
189
|
-
const hash = crypto.createHash('sha256').update(`${channelKey}\0${interactionId}`).digest('hex').slice(0, 32);
|
|
190
|
-
return `ec_${hash}`;
|
|
191
|
-
}
|