larkin 0.2.70
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/CONTRIBUTING.md +48 -0
- package/LICENSE +201 -0
- package/README.md +122 -0
- package/SECURITY.md +14 -0
- package/artifacts/release/THIRD_PARTY_NOTICES.txt +5488 -0
- package/dist/agent/agent-cli-capabilities.cjs +43 -0
- package/dist/agent/agent-cli-capabilities.mjs +35 -0
- package/dist/agent/agent-state-store.cjs +961 -0
- package/dist/agent/agent-state-store.mjs +923 -0
- package/dist/agent/agent-transport.cjs +201 -0
- package/dist/agent/agent-transport.mjs +194 -0
- package/dist/agent/config-cli-contract.cjs +19 -0
- package/dist/agent/config-cli-contract.mjs +16 -0
- package/dist/agent/context-prompt.cjs +159 -0
- package/dist/agent/context-prompt.mjs +155 -0
- package/dist/agent/freshness-gate.mjs +24 -0
- package/dist/agent/history-projection.cjs +72 -0
- package/dist/agent/history-projection.mjs +65 -0
- package/dist/agent/host-reminder-orchestrator.cjs +289 -0
- package/dist/agent/host-reminder-orchestrator.mjs +252 -0
- package/dist/agent/inbox-projection.cjs +92 -0
- package/dist/agent/inbox-projection.mjs +85 -0
- package/dist/agent/interaction-state-machine.cjs +710 -0
- package/dist/agent/interaction-state-machine.mjs +705 -0
- package/dist/agent/reminder-routes.cjs +192 -0
- package/dist/agent/reminder-routes.mjs +156 -0
- package/dist/agent/reminder-store.cjs +220 -0
- package/dist/agent/reminder-store.mjs +205 -0
- package/dist/agent/transport-business-context.cjs +486 -0
- package/dist/agent/transport-business-context.mjs +446 -0
- package/dist/agent/transport-shell.cjs +45 -0
- package/dist/agent/transport-shell.mjs +39 -0
- package/dist/app/agent-cli.mjs +596 -0
- package/dist/app/agent-config.mjs +633 -0
- package/dist/app/agent-lark-cli-workspace.cjs +95 -0
- package/dist/app/agent-lark-cli-workspace.mjs +85 -0
- package/dist/app/agent-readiness.cjs +42 -0
- package/dist/app/agent-readiness.mjs +38 -0
- package/dist/app/binary-entry.mjs +86 -0
- package/dist/app/cli.mjs +176 -0
- package/dist/app/dashboard.mjs +205 -0
- package/dist/app/internal-command.cjs +57 -0
- package/dist/app/internal-command.mjs +48 -0
- package/dist/app/lark-channel-secret.mjs +22 -0
- package/dist/app/lark-cli.mjs +887 -0
- package/dist/app/lark.mjs +45 -0
- package/dist/app/local-control.mjs +728 -0
- package/dist/app/official-lark-cli.cjs +138 -0
- package/dist/app/official-lark-cli.mjs +128 -0
- package/dist/app/pi-auth-cli.mjs +67 -0
- package/dist/app/run.mjs +364 -0
- package/dist/app/runtime-agent-config.mjs +416 -0
- package/dist/app/runtime-model-directory.mjs +78 -0
- package/dist/app/runtime-process.mjs +113 -0
- package/dist/app/session-cli.mjs +83 -0
- package/dist/app/setup-dashboard.mjs +26 -0
- package/dist/app/setup.mjs +166 -0
- package/dist/app/telemetry.mjs +83 -0
- package/dist/dashboard/dashboard-assets.mjs +21 -0
- package/dist/dashboard/dashboard-avatar.mjs +128 -0
- package/dist/dashboard/dashboard-config-controller.mjs +525 -0
- package/dist/dashboard/dashboard-lifecycle.cjs +97 -0
- package/dist/dashboard/dashboard-lifecycle.mjs +88 -0
- package/dist/dashboard/dashboard-template.mjs +29 -0
- package/dist/dashboard/dashboard-view-model.mjs +556 -0
- package/dist/dashboard/dashboard-workspace.mjs +104 -0
- package/dist/dashboard/web/assets/dashboard.css +3 -0
- package/dist/dashboard/web/assets/dashboard.js +49 -0
- package/dist/feishu/channel-transport.cjs +151 -0
- package/dist/feishu/channel-transport.mjs +148 -0
- package/dist/feishu/document-comment.cjs +120 -0
- package/dist/feishu/document-comment.mjs +109 -0
- package/dist/feishu/host-business-state.cjs +477 -0
- package/dist/feishu/host-business-state.mjs +462 -0
- package/dist/feishu/host-channel-business.cjs +161 -0
- package/dist/feishu/host-channel-business.mjs +157 -0
- package/dist/feishu/host-processing-eye.cjs +185 -0
- package/dist/feishu/host-processing-eye.mjs +181 -0
- package/dist/feishu/host-shell.cjs +1425 -0
- package/dist/feishu/host-shell.mjs +1385 -0
- package/dist/feishu/im-freshness-adapter.mjs +89 -0
- package/dist/feishu/interaction-orchestrator.cjs +279 -0
- package/dist/feishu/interaction-orchestrator.mjs +242 -0
- package/dist/feishu/lark-passthrough.mjs +70 -0
- package/dist/feishu/message-policy.cjs +111 -0
- package/dist/feishu/message-policy.mjs +98 -0
- package/dist/feishu/outbound-transport.cjs +221 -0
- package/dist/feishu/outbound-transport.mjs +215 -0
- package/dist/platform/build-info.cjs +25 -0
- package/dist/platform/build-info.mjs +18 -0
- package/dist/platform/callback-capability.cjs +301 -0
- package/dist/platform/callback-capability.mjs +252 -0
- package/dist/platform/check-bun-version.cjs +12 -0
- package/dist/platform/config.cjs +751 -0
- package/dist/platform/config.mjs +694 -0
- package/dist/platform/process-inspect.cjs +89 -0
- package/dist/platform/process-state.cjs +237 -0
- package/dist/platform/process-state.mjs +223 -0
- package/dist/platform/release-artifacts.mjs +61 -0
- package/dist/platform/root-layout.cjs +111 -0
- package/dist/platform/root-layout.mjs +72 -0
- package/dist/platform/telemetry-config.cjs +64 -0
- package/dist/platform/telemetry-config.mjs +56 -0
- package/dist/platform/telemetry-spool.cjs +661 -0
- package/dist/platform/telemetry-spool.mjs +653 -0
- package/dist/platform/telemetry-tracing.cjs +602 -0
- package/dist/platform/telemetry-tracing.mjs +596 -0
- package/dist/platform/telemetry-uploader.cjs +150 -0
- package/dist/platform/telemetry-uploader.mjs +146 -0
- package/dist/platform/workspace-service.cjs +469 -0
- package/dist/platform/workspace-service.mjs +430 -0
- package/dist/runtime/builtin-pi-assets.mjs +33 -0
- package/dist/runtime/claude-model-catalog.cjs +102 -0
- package/dist/runtime/claude-model-catalog.mjs +99 -0
- package/dist/runtime/codex-model-catalog.cjs +108 -0
- package/dist/runtime/codex-model-catalog.mjs +105 -0
- package/dist/runtime/pi-bash-timeout-extension.mjs +60 -0
- package/dist/runtime/pi-bash-timeout-injection.mjs +60 -0
- package/dist/runtime/pi-bash-timeout.bundle.js +43 -0
- package/dist/runtime/pi-model-catalog.cjs +116 -0
- package/dist/runtime/pi-model-catalog.mjs +75 -0
- package/dist/runtime/pi-official-auth.mjs +480 -0
- package/dist/runtime/pi-provider-config.cjs +252 -0
- package/dist/runtime/pi-provider-config.mjs +239 -0
- package/dist/runtime/pi-rpc-client.cjs +255 -0
- package/dist/runtime/pi-rpc-client.mjs +251 -0
- package/dist/runtime/pi-subagent-injection.mjs +129 -0
- package/dist/runtime/pi-subagents.bundle.js +8990 -0
- package/dist/runtime/runtime-adapters.mjs +1019 -0
- package/dist/runtime/runtime-contracts.cjs +2 -0
- package/dist/runtime/runtime-contracts.mjs +1 -0
- package/dist/runtime/runtime-host.cjs +930 -0
- package/dist/runtime/runtime-host.mjs +922 -0
- package/dist/runtime/runtime-model-catalog.cjs +43 -0
- package/dist/runtime/runtime-model-catalog.mjs +40 -0
- package/dist/runtime/runtime-readiness.cjs +177 -0
- package/dist/runtime/runtime-readiness.mjs +133 -0
- package/dist/setup/bot-register.mjs +655 -0
- package/dist/setup/grant-scopes.mjs +133 -0
- package/dist/setup/run-credential-preflight.mjs +89 -0
- package/dist/setup/setup-agent-choice.mjs +172 -0
- package/dist/setup/setup-bind.mjs +337 -0
- package/dist/setup/setup-binding.mjs +52 -0
- package/package.json +112 -0
|
@@ -0,0 +1,1425 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.memberNamesFromPayloads = memberNamesFromPayloads;
|
|
40
|
+
exports.createHostShell = createHostShell;
|
|
41
|
+
const node_child_process_1 = require("node:child_process");
|
|
42
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
43
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
44
|
+
const node_readline_1 = __importDefault(require("node:readline"));
|
|
45
|
+
const channelSdk = __importStar(require("@larksuite/channel"));
|
|
46
|
+
const api_1 = require("@opentelemetry/api");
|
|
47
|
+
const process_inspect_cjs_1 = require("../platform/process-inspect.cjs");
|
|
48
|
+
const workspace_service_js_1 = require("../platform/workspace-service.cjs");
|
|
49
|
+
const agent_state_store_js_1 = require("../agent/agent-state-store.cjs");
|
|
50
|
+
const host_business_state_js_1 = require("./host-business-state.cjs");
|
|
51
|
+
const host_processing_eye_js_1 = require("./host-processing-eye.cjs");
|
|
52
|
+
const inbox_projection_js_1 = require("../agent/inbox-projection.cjs");
|
|
53
|
+
const host_reminder_orchestrator_js_1 = require("../agent/host-reminder-orchestrator.cjs");
|
|
54
|
+
const host_channel_business_js_1 = require("./host-channel-business.cjs");
|
|
55
|
+
const interaction_orchestrator_js_1 = require("./interaction-orchestrator.cjs");
|
|
56
|
+
const message_policy_js_1 = require("./message-policy.cjs");
|
|
57
|
+
const runtime_readiness_js_1 = require("../runtime/runtime-readiness.cjs");
|
|
58
|
+
const callback_capability_js_1 = require("../platform/callback-capability.cjs");
|
|
59
|
+
const config_js_1 = require("../platform/config.cjs");
|
|
60
|
+
const internal_command_js_1 = require("../app/internal-command.cjs");
|
|
61
|
+
const agent_lark_cli_workspace_js_1 = require("../app/agent-lark-cli-workspace.cjs");
|
|
62
|
+
const agent_readiness_js_1 = require("../app/agent-readiness.cjs");
|
|
63
|
+
const document_comment_js_1 = require("./document-comment.cjs");
|
|
64
|
+
const PENDING_DOCUMENT_COMMENT_LIMIT = 256;
|
|
65
|
+
function isRecord(value) {
|
|
66
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
67
|
+
}
|
|
68
|
+
function classifyDocumentCommentReadFailure(error) {
|
|
69
|
+
const root = isRecord(error) ? error : null;
|
|
70
|
+
const response = isRecord(root?.response) ? root.response : null;
|
|
71
|
+
const data = isRecord(response?.data) ? response.data : null;
|
|
72
|
+
const nested = isRecord(root?.error) ? root.error : null;
|
|
73
|
+
const code = data?.code ?? nested?.code ?? root?.code;
|
|
74
|
+
const subtype = data?.subtype ?? nested?.subtype ?? root?.subtype;
|
|
75
|
+
const missingScopes = data?.missing_scopes ?? nested?.missing_scopes ?? root?.missing_scopes;
|
|
76
|
+
if (code === 99991672 || subtype === "app_scope_not_applied" || (Array.isArray(missingScopes) && missingScopes.length > 0)) {
|
|
77
|
+
return { category: "permission_missing", reason: "required_scope_missing" };
|
|
78
|
+
}
|
|
79
|
+
if (code === "permission_denied" || response?.status === 403 || root?.status === 403) {
|
|
80
|
+
return { category: "document_access_denied", reason: "bot_document_access_denied" };
|
|
81
|
+
}
|
|
82
|
+
return { category: "read_failure_unknown", reason: "provider_read_failed_unknown" };
|
|
83
|
+
}
|
|
84
|
+
function memberPayloadData(value) {
|
|
85
|
+
if (!isRecord(value))
|
|
86
|
+
return null;
|
|
87
|
+
let data = null;
|
|
88
|
+
if (value.ok === true && isRecord(value.data))
|
|
89
|
+
data = value.data;
|
|
90
|
+
else if (value.code === 0 && isRecord(value.data))
|
|
91
|
+
data = value.data;
|
|
92
|
+
if (data && isRecord(data.data))
|
|
93
|
+
data = data.data;
|
|
94
|
+
return data;
|
|
95
|
+
}
|
|
96
|
+
/** Parse both raw OpenAPI and lark-cli wrapped member/bot response shapes. */
|
|
97
|
+
function memberNamesFromPayloads(payloads) {
|
|
98
|
+
const names = {};
|
|
99
|
+
for (const payload of payloads) {
|
|
100
|
+
const data = memberPayloadData(payload);
|
|
101
|
+
if (!data)
|
|
102
|
+
continue;
|
|
103
|
+
const rows = ["items", "users", "bots", "bot_infos"].flatMap((key) => Array.isArray(data[key]) ? data[key] : []);
|
|
104
|
+
for (const row of rows) {
|
|
105
|
+
if (!isRecord(row))
|
|
106
|
+
continue;
|
|
107
|
+
const name = String(row.name || row.bot_name || row.member_id || row.open_id || row.app_id || "");
|
|
108
|
+
if (!name)
|
|
109
|
+
continue;
|
|
110
|
+
for (const key of ["member_id", "open_id", "app_id", "bot_id"]) {
|
|
111
|
+
if (row[key])
|
|
112
|
+
names[String(row[key])] = name;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return names;
|
|
117
|
+
}
|
|
118
|
+
function errorMessage(error) { return error instanceof Error ? error.message : String(error); }
|
|
119
|
+
function agentConfigSignature(agent) {
|
|
120
|
+
return JSON.stringify({
|
|
121
|
+
agentId: agent.agentId, runtime: agent.runtime, model: agent.model, piDistribution: agent.piDistribution ?? null, effort: agent.effort ?? null,
|
|
122
|
+
feishuAppId: agent.feishuAppId, feishuProfile: agent.feishuProfile,
|
|
123
|
+
larkConfigDir: agent.larkConfigDir, feishuDomain: agent.feishuDomain,
|
|
124
|
+
feishuAppSecret: agent.feishuAppSecret, workspaceDir: agent.workspaceDir, stateDir: agent.stateDir,
|
|
125
|
+
noMentionChats: agent.noMentionChats || [],
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function loadAgents(env, testEventSource, larkinHome, reconcileAgentWorkspaceImpl) {
|
|
129
|
+
if (!env.LARKIN_AGENTS_CONFIG)
|
|
130
|
+
throw new Error("LARKIN_AGENTS_CONFIG 缺少,必须提供非空 Agent 数组");
|
|
131
|
+
let parsed;
|
|
132
|
+
try {
|
|
133
|
+
parsed = JSON.parse(env.LARKIN_AGENTS_CONFIG);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
throw new Error(`LARKIN_AGENTS_CONFIG JSON 解析失败: ${errorMessage(error)}`);
|
|
137
|
+
}
|
|
138
|
+
if (!Array.isArray(parsed) || parsed.length === 0)
|
|
139
|
+
throw new Error("LARKIN_AGENTS_CONFIG 必须是非空 Agent 数组");
|
|
140
|
+
const trustedWorkspaceRoot = node_path_1.default.join(larkinHome, "agents");
|
|
141
|
+
return parsed.map((candidate) => {
|
|
142
|
+
if (!isRecord(candidate))
|
|
143
|
+
throw new Error("LARKIN_AGENTS_CONFIG Agent 必须是对象");
|
|
144
|
+
const id = candidate.agentId;
|
|
145
|
+
if (typeof id !== "string" || !/^cli_[A-Za-z0-9]+$/.test(id)
|
|
146
|
+
|| candidate.name !== id || candidate.feishuAppId !== id || candidate.feishuProfile !== id) {
|
|
147
|
+
throw new Error("LARKIN_AGENTS_CONFIG Agent identity 必须全部等于合法 App ID");
|
|
148
|
+
}
|
|
149
|
+
for (const field of ["runtime", "model"]) {
|
|
150
|
+
if (typeof candidate[field] !== "string" || !candidate[field]) {
|
|
151
|
+
throw new Error(`LARKIN_AGENTS_CONFIG Agent ${id}.${field} 必须是非空字符串`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (!testEventSource && (typeof candidate.feishuAppSecret !== "string" || !candidate.feishuAppSecret.trim()
|
|
155
|
+
|| !["https://open.feishu.cn", "https://open.larksuite.com"].includes(String(candidate.feishuDomain)))) {
|
|
156
|
+
throw new Error(`LARKIN_AGENTS_CONFIG Agent ${id} 缺少有效 channel 凭证/domain`);
|
|
157
|
+
}
|
|
158
|
+
const expectedWorkspace = node_path_1.default.join(larkinHome, "agents", id);
|
|
159
|
+
const expectedState = node_path_1.default.join(larkinHome, "state", "agents", id);
|
|
160
|
+
if (node_path_1.default.resolve(String(candidate.workspaceDir || "")) !== expectedWorkspace
|
|
161
|
+
|| node_path_1.default.resolve(String(candidate.stateDir || "")) !== expectedState) {
|
|
162
|
+
throw new Error(`LARKIN_AGENTS_CONFIG Agent ${id} workspaceDir/stateDir 不是 canonical single-root 路径`);
|
|
163
|
+
}
|
|
164
|
+
reconcileAgentWorkspaceImpl({ workspaceDir: expectedWorkspace, trustedWorkspaceRoot, lockDir: expectedState, agentId: id });
|
|
165
|
+
return candidate;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
function createHostShell({ env = process.env, runtimeHost, channelPackage, eventSourceStartDelayMs = 2_000, channelDisconnectTimeoutMs = 2_000, execFileImpl = node_child_process_1.execFile, managedCliForAgent = (agent) => (0, agent_lark_cli_workspace_js_1.managedOfficialLarkCli)(agent, env), reconcileAgentWorkspaceImpl = workspace_service_js_1.reconcileAgentWorkspace, stateStoreForImpl = agent_state_store_js_1.createAgentStateStore, logImpl = (...parts) => { process.stderr.write(`[host] ${parts.join(" ")}\n`); }, onOrderedShutdownComplete, telemetry, }) {
|
|
169
|
+
const eventCommand = env.LARKIN_FEISHU_EVENT_CMD || "";
|
|
170
|
+
const eventFile = env.LARKIN_FEISHU_EVENT_FILE || "";
|
|
171
|
+
const testEventSource = Boolean(eventCommand || eventFile);
|
|
172
|
+
if (testEventSource && env.LARKIN_FEISHU_DRYRUN !== "1") {
|
|
173
|
+
throw new Error("LARKIN_FEISHU_EVENT_CMD/FILE 是测试注入,仅允许在 LARKIN_FEISHU_DRYRUN=1 时使用");
|
|
174
|
+
}
|
|
175
|
+
const larkinHome = env.LARKIN_HOME;
|
|
176
|
+
const configDir = env.LARKIN_CONFIG_DIR;
|
|
177
|
+
const serverId = env.LARKIN_SERVER_ID;
|
|
178
|
+
if (!larkinHome || !configDir)
|
|
179
|
+
throw new Error("LARKIN_HOME 和 LARKIN_CONFIG_DIR 是必需的 root 配置");
|
|
180
|
+
if (node_path_1.default.resolve(larkinHome) !== node_path_1.default.resolve(configDir))
|
|
181
|
+
throw new Error("LARKIN_HOME 与 LARKIN_CONFIG_DIR 必须是同一个 root");
|
|
182
|
+
if (!serverId)
|
|
183
|
+
throw new Error("LARKIN_SERVER_ID 是必需的 strict server identity");
|
|
184
|
+
const daemonStartedAt = new Date().toISOString();
|
|
185
|
+
const agents = loadAgents(env, testEventSource, larkinHome, reconcileAgentWorkspaceImpl);
|
|
186
|
+
const log = (...parts) => { logImpl(...parts); };
|
|
187
|
+
const stores = new Map();
|
|
188
|
+
const stateStore = (subject) => {
|
|
189
|
+
const agent = agents.find((candidate) => candidate.agentId === subject.agentId)
|
|
190
|
+
?? ("stateDir" in subject ? subject : null);
|
|
191
|
+
if (!agent)
|
|
192
|
+
throw new Error(`未知 Agent state store: ${subject.agentId}`);
|
|
193
|
+
let store = stores.get(agent.agentId);
|
|
194
|
+
if (!store) {
|
|
195
|
+
store = stateStoreForImpl(larkinHome, agent.agentId);
|
|
196
|
+
if (node_path_1.default.resolve(store.paths.root) !== node_path_1.default.resolve(agent.stateDir))
|
|
197
|
+
throw new Error(`Agent ${agent.agentId} state store 路径不一致`);
|
|
198
|
+
stores.set(agent.agentId, store);
|
|
199
|
+
}
|
|
200
|
+
return store;
|
|
201
|
+
};
|
|
202
|
+
const hostState = new host_business_state_js_1.HostStateProjection(stateStore, log);
|
|
203
|
+
const agentStates = new Map();
|
|
204
|
+
const saveAgentState = (record) => {
|
|
205
|
+
try {
|
|
206
|
+
record.store.writeJson("agentState", record.state);
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
log(`agent-state 写失败: ${errorMessage(error)}`);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
const initializeAgentState = (agent) => {
|
|
213
|
+
const existing = agentStates.get(agent.agentId);
|
|
214
|
+
if (existing)
|
|
215
|
+
return existing;
|
|
216
|
+
const store = stateStore(agent);
|
|
217
|
+
let state;
|
|
218
|
+
try {
|
|
219
|
+
const loaded = store.readJson("agentState", {});
|
|
220
|
+
state = { ...loaded, sessions: isRecord(loaded.sessions) ? loaded.sessions : {} };
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
state = { sessions: {} };
|
|
224
|
+
}
|
|
225
|
+
state.agentId = agent.agentId;
|
|
226
|
+
const record = { store, state };
|
|
227
|
+
agentStates.set(agent.agentId, record);
|
|
228
|
+
saveAgentState(record);
|
|
229
|
+
log(`agent 身份: name=${agent.name} agentId=${agent.agentId} sessions=${JSON.stringify(state.sessions)}`);
|
|
230
|
+
return record;
|
|
231
|
+
};
|
|
232
|
+
for (const agent of agents)
|
|
233
|
+
initializeAgentState(agent);
|
|
234
|
+
const processingEyes = new host_processing_eye_js_1.ProcessingEyeOrchestrator({
|
|
235
|
+
execFile: execFileImpl,
|
|
236
|
+
log,
|
|
237
|
+
recordStatusError: (agent, text) => hostState.recordStatusError(agent, text),
|
|
238
|
+
readPending: (agent) => stateStore(agent).readJson("pendingReact", {}).items || [],
|
|
239
|
+
writePending: (agent, items) => stateStore(agent).writeJson("pendingReact", { items }),
|
|
240
|
+
cliForAgent: (agent) => {
|
|
241
|
+
const managed = managedCliForAgent(agent);
|
|
242
|
+
return { command: managed.command.command, argsPrefix: managed.command.argsPrefix, env: managed.env };
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
const larkApi = (agent, method, apiPath, data) => new Promise((resolve) => processingEyes.larkApi(agent, method, apiPath, data, (_error, result) => resolve(result)));
|
|
246
|
+
const fetchMemberPayload = (agent, args) => new Promise((resolve) => {
|
|
247
|
+
const managed = managedCliForAgent(agent);
|
|
248
|
+
execFileImpl(managed.command.command, [...managed.command.argsPrefix, "im", ...args, "--json"], {
|
|
249
|
+
encoding: "utf8",
|
|
250
|
+
timeout: 10_000,
|
|
251
|
+
env: managed.env,
|
|
252
|
+
}, (error, stdout) => {
|
|
253
|
+
if (error)
|
|
254
|
+
log(`成员表子查询失败 agent=${agent.name}: ${errorMessage(error).slice(0, 100)}`);
|
|
255
|
+
try {
|
|
256
|
+
resolve(JSON.parse(String(stdout)));
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
resolve(null);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
const fetchChatNames = async (agent, chatId) => {
|
|
264
|
+
const payloads = await Promise.all([
|
|
265
|
+
fetchMemberPayload(agent, ["chat.members", "get", "--chat-id", chatId, "--member-id-type", "open_id", "--page-all"]),
|
|
266
|
+
fetchMemberPayload(agent, ["chat.members", "bots", "--chat-id", chatId]),
|
|
267
|
+
]);
|
|
268
|
+
const names = memberNamesFromPayloads(payloads);
|
|
269
|
+
if (!Object.keys(names).length) {
|
|
270
|
+
log(`成员表拉取失败 agent=${agent.name} chat=${chatId}`);
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
return names;
|
|
274
|
+
};
|
|
275
|
+
const signatureTtl = 6 * 60 * 60 * 1000;
|
|
276
|
+
const signatureFailTtl = 10 * 60 * 1000;
|
|
277
|
+
const signatureNotFoundTtl = 7 * 24 * 60 * 60 * 1000;
|
|
278
|
+
const fetchSenderSignature = async (agent, openId) => {
|
|
279
|
+
const response = await larkApi(agent, "GET", `/open-apis/contact/v3/users/${openId}?user_id_type=open_id`, null);
|
|
280
|
+
const data = response && isRecord(response.data) ? response.data : null;
|
|
281
|
+
const user = data && isRecord(data.user) ? data.user : null;
|
|
282
|
+
if (!user)
|
|
283
|
+
return { desc: null, name: null, ok: false, ttl: response ? signatureNotFoundTtl : signatureFailTtl };
|
|
284
|
+
return {
|
|
285
|
+
desc: String(user.description || "").trim() || null,
|
|
286
|
+
name: user.name ? String(user.name) : null,
|
|
287
|
+
ok: true,
|
|
288
|
+
ttl: signatureTtl,
|
|
289
|
+
};
|
|
290
|
+
};
|
|
291
|
+
const senderIdentity = new host_business_state_js_1.SenderIdentityCache({
|
|
292
|
+
state: hostState,
|
|
293
|
+
fetchChatNames,
|
|
294
|
+
fetchSenderSignature,
|
|
295
|
+
signatureFailureTtlMs: signatureFailTtl,
|
|
296
|
+
});
|
|
297
|
+
const envelopeProjector = new host_business_state_js_1.HostEnvelopeProjector(hostState, (agent, chatId, senderId) => senderIdentity.noteUnknownSender(agent, chatId, senderId), undefined, undefined, "larkin");
|
|
298
|
+
const hydrateBotIdentity = (agent) => {
|
|
299
|
+
if (!agent.botOpenId) {
|
|
300
|
+
try {
|
|
301
|
+
const cached = stateStore(agent).readJson("botIdentity", {});
|
|
302
|
+
if (cached.open_id) {
|
|
303
|
+
agent.botOpenId = cached.open_id;
|
|
304
|
+
agent.botName = cached.name || null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
catch { /* first run */ }
|
|
308
|
+
}
|
|
309
|
+
agent.displayName = agent.botName || agent.displayName || null;
|
|
310
|
+
};
|
|
311
|
+
const prepareAgentState = (agent) => {
|
|
312
|
+
node_fs_1.default.mkdirSync(agent.stateDir, { recursive: true });
|
|
313
|
+
hydrateBotIdentity(agent);
|
|
314
|
+
processingEyes.restoreAndClear(agent);
|
|
315
|
+
try {
|
|
316
|
+
const known = new Set(Object.values(hostState.loadMap(agent)).filter((value) => /^oc_/.test(String(value))));
|
|
317
|
+
for (const chatId of known)
|
|
318
|
+
void senderIdentity.ensureChatNames(agent, chatId, 0);
|
|
319
|
+
}
|
|
320
|
+
catch { /* no map */ }
|
|
321
|
+
senderIdentity.warmSenderProfiles(agent);
|
|
322
|
+
};
|
|
323
|
+
for (const agent of agents)
|
|
324
|
+
prepareAgentState(agent);
|
|
325
|
+
const reminder = new host_reminder_orchestrator_js_1.HostReminderOrchestrator({ agents, stateStore, envelopeProjector, deliveryTarget: runtimeHost, log });
|
|
326
|
+
const seenEventIds = new Set();
|
|
327
|
+
const inFlightEventIds = new Set();
|
|
328
|
+
const onFeishuMessage = async (agent, event, options) => {
|
|
329
|
+
const wake = options?.wake !== false;
|
|
330
|
+
const eventKey = `${agent.agentId}:${event.event_id || event.message_id || ""}`;
|
|
331
|
+
if (event.event_id && (seenEventIds.has(eventKey) || inFlightEventIds.has(eventKey)))
|
|
332
|
+
return;
|
|
333
|
+
if (agent.botOpenId && event.sender_id === agent.botOpenId) {
|
|
334
|
+
log(`agent=${agent.name} 跳过自己发的消息`);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const telemetryMessageId = String(event.message_id || event.event_id || eventKey);
|
|
338
|
+
if (wake)
|
|
339
|
+
telemetry?.beginMessage(agent.agentId, telemetryMessageId);
|
|
340
|
+
if (event.event_id)
|
|
341
|
+
inFlightEventIds.add(eventKey);
|
|
342
|
+
try {
|
|
343
|
+
const receive = async () => {
|
|
344
|
+
const [names, signature] = await Promise.all([
|
|
345
|
+
senderIdentity.ensureChatNames(agent, event.chat_id, 3_000),
|
|
346
|
+
event._sender_is_bot ? Promise.resolve(null) : senderIdentity.ensureSenderSignature(agent, event.sender_id, 3_000),
|
|
347
|
+
]);
|
|
348
|
+
const envelope = envelopeProjector.projectInbound(agent, event, { anchorReply: wake, names, signature });
|
|
349
|
+
envelope.target = (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)({ ...envelope, chat_id: event.chat_id, thread_id: event.thread_id });
|
|
350
|
+
if (wake)
|
|
351
|
+
envelope.wake = true;
|
|
352
|
+
const inboxEnvelope = (0, inbox_projection_js_1.projectInboxEnvelope)(envelope, {
|
|
353
|
+
chat_id: event.chat_id,
|
|
354
|
+
thread_id: event.thread_id,
|
|
355
|
+
...(event.create_time !== undefined ? { create_time: String(event.create_time) } : {}),
|
|
356
|
+
sender_id: event.sender_id,
|
|
357
|
+
content: String(envelope.content ?? event.content ?? ""),
|
|
358
|
+
});
|
|
359
|
+
try {
|
|
360
|
+
stateStore(agent).appendNdjson("inbox", inboxEnvelope);
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
throw new Error(`inbox 写失败: ${errorMessage(error)}`);
|
|
364
|
+
}
|
|
365
|
+
// An event becomes permanently seen only after its canonical Inbox append
|
|
366
|
+
// is durable. Failures remain eligible for same-process redelivery.
|
|
367
|
+
if (event.event_id)
|
|
368
|
+
seenEventIds.add(eventKey);
|
|
369
|
+
hostState.appendConversation(agent, {
|
|
370
|
+
direction: "in", from: envelope.sender_name, senderType: envelope.sender_type,
|
|
371
|
+
target: (0, message_policy_js_1.targetFor)(event).target, wake, text: event.content, messageId: envelope.message_id,
|
|
372
|
+
at: envelope.timestamp || new Date().toISOString(),
|
|
373
|
+
});
|
|
374
|
+
return envelope;
|
|
375
|
+
};
|
|
376
|
+
const envelope = wake && telemetry
|
|
377
|
+
? await telemetry.phase(telemetryMessageId, "feishu.receive", api_1.SpanKind.CONSUMER, receive)
|
|
378
|
+
: await receive();
|
|
379
|
+
if (!wake)
|
|
380
|
+
return;
|
|
381
|
+
const receipt = await runtimeHost.deliver(agent.agentId, envelope);
|
|
382
|
+
if (receipt.status === "accepted" || receipt.status === "duplicate" || receipt.status === "deferred") {
|
|
383
|
+
hostState.appendStatusLog(agent, "deliverLog", {
|
|
384
|
+
from: envelope.sender_name,
|
|
385
|
+
target: (0, message_policy_js_1.targetFor)(event).target,
|
|
386
|
+
excerpt: (0, host_business_state_js_1.safeConversationExcerpt)(event.content, 180),
|
|
387
|
+
at: new Date().toISOString(),
|
|
388
|
+
}, 30);
|
|
389
|
+
if (envelope.sender_type === "human")
|
|
390
|
+
processingEyes.add(agent, String(envelope.message_id || ""));
|
|
391
|
+
if (receipt.status === "deferred")
|
|
392
|
+
log(`Runtime 暂缓投递,消息保留在 inbox seq=${envelope.seq}: ${receipt.reason}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
if (wake)
|
|
397
|
+
telemetry?.delivery(agent.agentId, telemetryMessageId, "error");
|
|
398
|
+
log(`onFeishuMessage 异常 agent=${agent.name}: ${error instanceof Error ? error.stack || error.message : String(error)}`);
|
|
399
|
+
hostState.recordStatusError(agent, `onFeishuMessage: ${errorMessage(error)}`);
|
|
400
|
+
}
|
|
401
|
+
finally {
|
|
402
|
+
if (event.event_id)
|
|
403
|
+
inFlightEventIds.delete(eventKey);
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
const interactionChannels = new Map();
|
|
407
|
+
const interaction = new interaction_orchestrator_js_1.HostInteractionOrchestrator({
|
|
408
|
+
agents,
|
|
409
|
+
stateStore,
|
|
410
|
+
deliveryTarget: runtimeHost,
|
|
411
|
+
channelFor: (agent) => interactionChannels.get(agent.agentId),
|
|
412
|
+
log,
|
|
413
|
+
});
|
|
414
|
+
const emptyPendingDocumentComments = () => ({ version: 3, items: {} });
|
|
415
|
+
const validPendingDocumentComment = (value) => {
|
|
416
|
+
if (!isRecord(value))
|
|
417
|
+
return false;
|
|
418
|
+
const validSubscription = (value.subscriptionMode === "subscribed"
|
|
419
|
+
&& value.subscriptionStatus === "platform-verified"
|
|
420
|
+
&& value.subscriptionSource === "platform-status"
|
|
421
|
+
&& value.subscriptionDimension === "application")
|
|
422
|
+
|| (value.subscriptionMode === "none" && value.subscriptionStatus === "safe-default"
|
|
423
|
+
&& (value.subscriptionSource === "legacy-default" || value.subscriptionSource === "setup-default")
|
|
424
|
+
&& value.subscriptionDimension === null)
|
|
425
|
+
|| (value.subscriptionMode === "none" && value.subscriptionStatus === "requested-unverified"
|
|
426
|
+
&& value.subscriptionSource === "setup-opt-in"
|
|
427
|
+
&& value.subscriptionDimension === "application");
|
|
428
|
+
return typeof value.messageId === "string" && /^doc_comment_[0-9a-f]{32}$/.test(value.messageId)
|
|
429
|
+
&& typeof value.fileToken === "string" && /^[A-Za-z0-9_-]+$/.test(value.fileToken)
|
|
430
|
+
&& typeof value.fileType === "string" && ["doc", "docx", "sheet", "file"].includes(value.fileType)
|
|
431
|
+
&& typeof value.commentId === "string" && /^[A-Za-z0-9_-]+$/.test(value.commentId)
|
|
432
|
+
&& (value.replyId === undefined || (typeof value.replyId === "string" && /^[A-Za-z0-9_-]+$/.test(value.replyId)))
|
|
433
|
+
&& typeof value.operatorOpenId === "string" && !!value.operatorOpenId
|
|
434
|
+
&& Number.isSafeInteger(value.timestamp) && Number(value.timestamp) > 0
|
|
435
|
+
&& typeof value.noticeType === "string" && !!value.noticeType
|
|
436
|
+
&& validSubscription
|
|
437
|
+
&& typeof value.mentionedBot === "boolean"
|
|
438
|
+
&& (value.subscriptionMode !== "none" || value.mentionedBot === true)
|
|
439
|
+
&& typeof value.queuedAt === "string" && Number.isFinite(Date.parse(value.queuedAt));
|
|
440
|
+
};
|
|
441
|
+
const normalizePendingDocumentComments = (value) => {
|
|
442
|
+
const normalized = emptyPendingDocumentComments();
|
|
443
|
+
const raw = isRecord(value) && isRecord(value.items) ? value.items : null;
|
|
444
|
+
if (!raw)
|
|
445
|
+
return normalized;
|
|
446
|
+
for (const [messageId, candidate] of Object.entries(raw)) {
|
|
447
|
+
if (Object.keys(normalized.items).length >= PENDING_DOCUMENT_COMMENT_LIMIT)
|
|
448
|
+
break;
|
|
449
|
+
if (validPendingDocumentComment(candidate) && candidate.messageId === messageId)
|
|
450
|
+
normalized.items[messageId] = candidate;
|
|
451
|
+
// Older rows whose acceptance depended on an IM mention policy cannot
|
|
452
|
+
// prove the new subscription boundary. Drop them during the bounded v3 upgrade
|
|
453
|
+
// of guessing authorization or allowing stale rows to consume capacity.
|
|
454
|
+
}
|
|
455
|
+
return normalized;
|
|
456
|
+
};
|
|
457
|
+
const withPendingDocumentComments = (agent, operation) => stateStore(agent).mutateJson("documentComments", emptyPendingDocumentComments(), (raw) => {
|
|
458
|
+
const normalized = normalizePendingDocumentComments(raw);
|
|
459
|
+
for (const key of Object.keys(raw))
|
|
460
|
+
delete raw[key];
|
|
461
|
+
Object.assign(raw, normalized);
|
|
462
|
+
return operation(raw);
|
|
463
|
+
});
|
|
464
|
+
const pendingRecord = (agent, event, subscription) => ({
|
|
465
|
+
messageId: (0, document_comment_js_1.documentCommentMessageId)(agent.agentId, event),
|
|
466
|
+
fileToken: event.fileToken,
|
|
467
|
+
fileType: event.fileType,
|
|
468
|
+
commentId: event.commentId,
|
|
469
|
+
...(event.replyId ? { replyId: event.replyId } : {}),
|
|
470
|
+
operatorOpenId: event.operator.openId,
|
|
471
|
+
timestamp: event.timestamp,
|
|
472
|
+
noticeType: (0, document_comment_js_1.documentCommentNoticeType)(event),
|
|
473
|
+
subscriptionMode: subscription.mode,
|
|
474
|
+
subscriptionStatus: subscription.status,
|
|
475
|
+
subscriptionSource: subscription.source,
|
|
476
|
+
subscriptionDimension: subscription.dimension,
|
|
477
|
+
mentionedBot: event.mentionedBot,
|
|
478
|
+
queuedAt: new Date().toISOString(),
|
|
479
|
+
});
|
|
480
|
+
const pendingEvent = (record) => ({
|
|
481
|
+
fileToken: record.fileToken,
|
|
482
|
+
fileType: record.fileType,
|
|
483
|
+
commentId: record.commentId,
|
|
484
|
+
...(record.replyId ? { replyId: record.replyId } : {}),
|
|
485
|
+
operator: { openId: record.operatorOpenId },
|
|
486
|
+
mentionedBot: record.mentionedBot,
|
|
487
|
+
timestamp: record.timestamp,
|
|
488
|
+
raw: { notice_type: record.noticeType },
|
|
489
|
+
});
|
|
490
|
+
const pendingDocumentComment = (agent, messageId) => withPendingDocumentComments(agent, (state) => state.items[messageId] ?? null);
|
|
491
|
+
const persistPendingDocumentComment = (agent, record) => withPendingDocumentComments(agent, (state) => {
|
|
492
|
+
const existing = state.items[record.messageId];
|
|
493
|
+
if (existing)
|
|
494
|
+
return existing;
|
|
495
|
+
if (Object.keys(state.items).length >= PENDING_DOCUMENT_COMMENT_LIMIT) {
|
|
496
|
+
throw Object.assign(new Error("document comment pending capacity exhausted"), { code: "PENDING_DOCUMENT_COMMENT_CAPACITY" });
|
|
497
|
+
}
|
|
498
|
+
state.items[record.messageId] = record;
|
|
499
|
+
return record;
|
|
500
|
+
});
|
|
501
|
+
const removePendingDocumentComment = (agent, messageId) => {
|
|
502
|
+
withPendingDocumentComments(agent, (state) => {
|
|
503
|
+
delete state.items[messageId];
|
|
504
|
+
});
|
|
505
|
+
};
|
|
506
|
+
const readPendingDocumentComments = (agent) => withPendingDocumentComments(agent, (state) => Object.values(state.items));
|
|
507
|
+
const recordDocumentCommentFailure = (agent, category, reason) => {
|
|
508
|
+
hostState.updateStatus(agent, {
|
|
509
|
+
documentCommentLastErrorAt: new Date().toISOString(),
|
|
510
|
+
documentCommentLastError: reason,
|
|
511
|
+
documentCommentLastErrorCategory: category,
|
|
512
|
+
});
|
|
513
|
+
};
|
|
514
|
+
const processPendingDocumentCommentOnce = async (agent, record, channel) => {
|
|
515
|
+
const event = pendingEvent(record);
|
|
516
|
+
let context;
|
|
517
|
+
try {
|
|
518
|
+
const resolve = () => (0, document_comment_js_1.resolveDocumentCommentContext)(channel.comments, event);
|
|
519
|
+
context = await (telemetry?.phase(record.messageId, "document.comment.resolve", api_1.SpanKind.CLIENT, resolve) ?? resolve());
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
const diagnostic = classifyDocumentCommentReadFailure(error);
|
|
523
|
+
recordDocumentCommentFailure(agent, diagnostic.category, diagnostic.reason);
|
|
524
|
+
throw new Error("评论正文读取失败(检查 scope、发布状态与 Bot 文档访问权)");
|
|
525
|
+
}
|
|
526
|
+
if (!context) {
|
|
527
|
+
removePendingDocumentComment(agent, record.messageId);
|
|
528
|
+
recordDocumentCommentFailure(agent, "comment_unavailable_or_empty", "comment_unavailable_or_empty");
|
|
529
|
+
log(`document comment 已终止处理 agent=${agent.name} reason=comment_unavailable_or_empty`);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const envelope = (0, document_comment_js_1.projectDocumentCommentEnvelope)({
|
|
533
|
+
agentId: agent.agentId,
|
|
534
|
+
event,
|
|
535
|
+
context,
|
|
536
|
+
subscription: {
|
|
537
|
+
mode: record.subscriptionMode,
|
|
538
|
+
status: record.subscriptionStatus,
|
|
539
|
+
source: record.subscriptionSource,
|
|
540
|
+
dimension: record.subscriptionDimension,
|
|
541
|
+
mentionedBot: record.mentionedBot,
|
|
542
|
+
},
|
|
543
|
+
});
|
|
544
|
+
let preparation;
|
|
545
|
+
try {
|
|
546
|
+
const persistInbox = async () => stateStore(agent).prepareInboxDelivery(envelope);
|
|
547
|
+
preparation = await (telemetry?.phase(record.messageId, "document.comment.inbox", api_1.SpanKind.PRODUCER, persistInbox) ?? persistInbox());
|
|
548
|
+
}
|
|
549
|
+
catch {
|
|
550
|
+
recordDocumentCommentFailure(agent, "inbox_write_failure", "canonical_inbox_write_failed");
|
|
551
|
+
throw new Error("document comment canonical Inbox 写入失败");
|
|
552
|
+
}
|
|
553
|
+
if (!["appended", "present"].includes(preparation)) {
|
|
554
|
+
removePendingDocumentComment(agent, record.messageId);
|
|
555
|
+
telemetry?.filterMessage(record.messageId, "duplicate");
|
|
556
|
+
log(`document comment 重复已去重 agent=${agent.name} state=${preparation}`);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
// Canonical Inbox/delivery state is now the recovery authority.
|
|
560
|
+
removePendingDocumentComment(agent, record.messageId);
|
|
561
|
+
const receipt = await runtimeHost.deliver(agent.agentId, envelope);
|
|
562
|
+
if (receipt.status === "error")
|
|
563
|
+
throw new Error("document comment Runtime 投递失败(canonical Inbox 已保留)");
|
|
564
|
+
hostState.updateStatus(agent, {
|
|
565
|
+
documentCommentLastAcceptedAt: new Date().toISOString(),
|
|
566
|
+
documentCommentLastError: null,
|
|
567
|
+
documentCommentLastErrorAt: null,
|
|
568
|
+
documentCommentLastErrorCategory: null,
|
|
569
|
+
});
|
|
570
|
+
log(`document comment ${preparation === "appended" ? "已入箱" : "补投"} agent=${agent.name} mode=${context.isWhole ? "top-level" : "in-thread"} delivery=${receipt.status}`);
|
|
571
|
+
};
|
|
572
|
+
const processingDocumentComments = new Map();
|
|
573
|
+
const processPendingDocumentComment = (agent, record, channel) => {
|
|
574
|
+
const key = `${agent.agentId}:${record.messageId}`;
|
|
575
|
+
const active = processingDocumentComments.get(key);
|
|
576
|
+
if (active)
|
|
577
|
+
return active;
|
|
578
|
+
const processing = processPendingDocumentCommentOnce(agent, record, channel)
|
|
579
|
+
.finally(() => processingDocumentComments.delete(key));
|
|
580
|
+
processingDocumentComments.set(key, processing);
|
|
581
|
+
return processing;
|
|
582
|
+
};
|
|
583
|
+
const replayingDocumentComments = new Map();
|
|
584
|
+
const replayPendingDocumentComments = (agent, channel) => {
|
|
585
|
+
const active = replayingDocumentComments.get(agent.agentId);
|
|
586
|
+
if (active)
|
|
587
|
+
return active;
|
|
588
|
+
const replay = (async () => {
|
|
589
|
+
for (const record of readPendingDocumentComments(agent)) {
|
|
590
|
+
try {
|
|
591
|
+
telemetry?.beginMessage(agent.agentId, record.messageId, "document_comment");
|
|
592
|
+
const replayOne = () => processPendingDocumentComment(agent, record, channel);
|
|
593
|
+
await (telemetry?.phase(record.messageId, "document.comment.replay", api_1.SpanKind.CONSUMER, replayOne) ?? replayOne());
|
|
594
|
+
}
|
|
595
|
+
catch {
|
|
596
|
+
log(`document comment 待处理记录仍需恢复 agent=${agent.name} reason=processing_failed`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
})().finally(() => replayingDocumentComments.delete(agent.agentId));
|
|
600
|
+
replayingDocumentComments.set(agent.agentId, replay);
|
|
601
|
+
return replay;
|
|
602
|
+
};
|
|
603
|
+
const onDocumentComment = async (agent, event, channel) => {
|
|
604
|
+
const messageId = (0, document_comment_js_1.documentCommentMessageId)(agent.agentId, event);
|
|
605
|
+
telemetry?.beginMessage(agent.agentId, messageId, "document_comment");
|
|
606
|
+
const receive = async () => undefined;
|
|
607
|
+
await (telemetry?.phase(messageId, "document.comment.receive", api_1.SpanKind.CONSUMER, receive) ?? receive());
|
|
608
|
+
const existing = pendingDocumentComment(agent, messageId);
|
|
609
|
+
if (existing) {
|
|
610
|
+
const replayOne = () => processPendingDocumentComment(agent, existing, channel);
|
|
611
|
+
await (telemetry?.phase(messageId, "document.comment.replay", api_1.SpanKind.CONSUMER, replayOne) ?? replayOne());
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
const gate = async () => {
|
|
615
|
+
let subscription;
|
|
616
|
+
try {
|
|
617
|
+
subscription = (0, callback_capability_js_1.readDocumentCommentSubscription)(larkinHome, agent.feishuProfile);
|
|
618
|
+
}
|
|
619
|
+
catch {
|
|
620
|
+
subscription = { mode: "none", status: "safe-default", source: "legacy-default", dimension: null };
|
|
621
|
+
log(`document comment subscription 状态不可读,使用安全默认 agent=${agent.name}`);
|
|
622
|
+
}
|
|
623
|
+
if (subscription.mode !== "subscribed" && !event.mentionedBot)
|
|
624
|
+
return { subscription, rejected: "subscription_unverified" };
|
|
625
|
+
if (!event.operator.openId || event.operator.openId === agent.botOpenId || event.operator.openId === channel.botIdentity?.openId) {
|
|
626
|
+
return { subscription, rejected: "self_or_missing_operator" };
|
|
627
|
+
}
|
|
628
|
+
if (!["doc", "docx", "sheet", "file"].includes(event.fileType))
|
|
629
|
+
return { subscription, rejected: "unsupported_file_type" };
|
|
630
|
+
return { subscription };
|
|
631
|
+
};
|
|
632
|
+
const gated = await (telemetry?.phase(messageId, "document.comment.gate", api_1.SpanKind.INTERNAL, gate) ?? gate());
|
|
633
|
+
const subscription = gated.subscription;
|
|
634
|
+
if (gated.rejected === "subscription_unverified") {
|
|
635
|
+
telemetry?.filterMessage(messageId, gated.rejected);
|
|
636
|
+
log(`document comment 未 @Bot 且无已验证订阅,忽略 agent=${agent.name} status=${subscription.status} source=${subscription.source}`);
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
if (gated.rejected === "self_or_missing_operator") {
|
|
640
|
+
telemetry?.filterMessage(messageId, gated.rejected);
|
|
641
|
+
log(`document comment 自身或缺少操作者,忽略 agent=${agent.name}`);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (gated.rejected === "unsupported_file_type") {
|
|
645
|
+
telemetry?.filterMessage(messageId, gated.rejected);
|
|
646
|
+
recordDocumentCommentFailure(agent, "comment_unavailable_or_empty", "unsupported_file_type");
|
|
647
|
+
log(`document comment 文件类型不支持,忽略 agent=${agent.name}`);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
const record = pendingRecord(agent, event, subscription);
|
|
651
|
+
let durableRecord;
|
|
652
|
+
try {
|
|
653
|
+
const persistPending = async () => persistPendingDocumentComment(agent, record);
|
|
654
|
+
durableRecord = await (telemetry?.phase(messageId, "document.comment.pending", api_1.SpanKind.PRODUCER, persistPending) ?? persistPending());
|
|
655
|
+
}
|
|
656
|
+
catch (error) {
|
|
657
|
+
if (error.code === "PENDING_DOCUMENT_COMMENT_CAPACITY") {
|
|
658
|
+
recordDocumentCommentFailure(agent, "pending_capacity_exhausted", "pending_capacity_exhausted");
|
|
659
|
+
throw new Error("document comment pending capacity exhausted;现有待处理记录已原样保留");
|
|
660
|
+
}
|
|
661
|
+
recordDocumentCommentFailure(agent, "pending_state_failure", "pending_record_write_failed");
|
|
662
|
+
throw new Error("document comment pending recovery record 写入失败");
|
|
663
|
+
}
|
|
664
|
+
await processPendingDocumentComment(agent, durableRecord, channel);
|
|
665
|
+
};
|
|
666
|
+
const channelBusiness = new host_channel_business_js_1.HostChannelBusiness({
|
|
667
|
+
state: hostState,
|
|
668
|
+
stateStore,
|
|
669
|
+
onMessage: onFeishuMessage,
|
|
670
|
+
onComment: (agent, event, channel) => onDocumentComment(agent, event, channel),
|
|
671
|
+
onReconnected: (agent, channel) => replayPendingDocumentComments(agent, channel),
|
|
672
|
+
mentionPolicy: (agentId, chatId) => {
|
|
673
|
+
try {
|
|
674
|
+
return (0, config_js_1.resolveMentionPolicy)((0, config_js_1.loadConfig)(env).config, agentId, chatId).effective;
|
|
675
|
+
}
|
|
676
|
+
catch (error) {
|
|
677
|
+
const startupAgent = agents.find((candidate) => candidate.agentId === agentId);
|
|
678
|
+
const fallback = startupAgent?.noMentionChats?.includes(chatId) ? "free" : "require";
|
|
679
|
+
log(`mention policy 刷新失败,使用已验证启动快照 agent=${agentId}: ${errorMessage(error)}`);
|
|
680
|
+
return fallback;
|
|
681
|
+
}
|
|
682
|
+
},
|
|
683
|
+
onCardAction: (agent, event) => {
|
|
684
|
+
const value = isRecord(event.action.value) ? event.action.value : null;
|
|
685
|
+
const nonce = typeof value?.larkin_callback_probe === "string" ? value.larkin_callback_probe : null;
|
|
686
|
+
if (nonce) {
|
|
687
|
+
const raw = isRecord(event.raw) ? event.raw : null;
|
|
688
|
+
const header = raw && isRecord(raw.header) ? raw.header : null;
|
|
689
|
+
const eventId = String(raw?.event_id || header?.event_id || "");
|
|
690
|
+
let verified = false;
|
|
691
|
+
try {
|
|
692
|
+
verified = (0, callback_capability_js_1.verifyCallbackProbe)(larkinHome, agent.agentId, nonce, eventId);
|
|
693
|
+
}
|
|
694
|
+
catch (error) {
|
|
695
|
+
log(`callback probe 验证失败 agent=${agent.name}: ${errorMessage(error)}`);
|
|
696
|
+
}
|
|
697
|
+
return { toast: { type: verified ? "success" : "error", content: verified
|
|
698
|
+
? "回调已真实送达,card.action.trigger 状态已验证生效。"
|
|
699
|
+
: "验证卡已失效或不匹配,请重新生成 callback-probe。" } };
|
|
700
|
+
}
|
|
701
|
+
return interaction.handleCardAction(agent, event);
|
|
702
|
+
},
|
|
703
|
+
log,
|
|
704
|
+
});
|
|
705
|
+
const startChannelSource = (agent, callbacks) => {
|
|
706
|
+
const { createLarkChannel } = channelPackage ?? channelSdk;
|
|
707
|
+
log(`入站(createLarkChannel): agent=${agent.name} app=${agent.feishuAppId} (群需@)`);
|
|
708
|
+
const channel = createLarkChannel({
|
|
709
|
+
appId: agent.feishuAppId,
|
|
710
|
+
appSecret: agent.feishuAppSecret,
|
|
711
|
+
domain: agent.feishuDomain || "https://open.feishu.cn",
|
|
712
|
+
source: "larkin",
|
|
713
|
+
policy: { dmMode: "open", requireMention: false, respondToMentionAll: true },
|
|
714
|
+
handshakeTimeoutMs: 15_000,
|
|
715
|
+
keepalive: {
|
|
716
|
+
enabled: true,
|
|
717
|
+
intervalMs: 15_000,
|
|
718
|
+
onUnrecoverable: (error) => {
|
|
719
|
+
log(`keepalive 无法恢复事件连接 agent=${agent.name}`);
|
|
720
|
+
callbacks.onFatal?.(error instanceof Error ? error : new Error("keepalive unrecoverable"));
|
|
721
|
+
},
|
|
722
|
+
},
|
|
723
|
+
includeRawEvent: true,
|
|
724
|
+
});
|
|
725
|
+
interactionChannels.set(agent.agentId, channel);
|
|
726
|
+
channel.on(channelBusiness.handlers(agent, channel));
|
|
727
|
+
channelBusiness.registerReadReceipts(agent, channel.dispatcher);
|
|
728
|
+
channel.connect().then(() => {
|
|
729
|
+
channelBusiness.registerCardActions(agent, channel.dispatcher);
|
|
730
|
+
return channelBusiness.connected(agent, channel, callbacks.onFatal).then(() => replayPendingDocumentComments(agent, channel));
|
|
731
|
+
})
|
|
732
|
+
.catch((error) => { log(`channel.connect 失败 agent=${agent.name}`); callbacks.onConnectFail?.(error); });
|
|
733
|
+
return channel;
|
|
734
|
+
};
|
|
735
|
+
let eventSourceStop = () => { };
|
|
736
|
+
let eventSourceStartTimer = null;
|
|
737
|
+
let requestOrderedShutdown = async () => { };
|
|
738
|
+
let hotUpsert = null;
|
|
739
|
+
const startEventSource = () => {
|
|
740
|
+
let shuttingDown = false;
|
|
741
|
+
const startChild = (agent, command, args, childEnv = process.env) => {
|
|
742
|
+
log(`自定义事件源: agent=${agent.name} command=${command}`);
|
|
743
|
+
const child = (0, node_child_process_1.spawn)(command, args, { stdio: ["pipe", "pipe", "inherit"], env: childEnv });
|
|
744
|
+
eventSourceStop = () => { try {
|
|
745
|
+
child.kill("SIGTERM");
|
|
746
|
+
}
|
|
747
|
+
catch { /* already exited */ } };
|
|
748
|
+
if (child.stdout)
|
|
749
|
+
node_readline_1.default.createInterface({ input: child.stdout }).on("line", (raw) => {
|
|
750
|
+
const line = raw.trim();
|
|
751
|
+
if (!line)
|
|
752
|
+
return;
|
|
753
|
+
try {
|
|
754
|
+
const parsed = JSON.parse(line);
|
|
755
|
+
const record = isRecord(parsed) ? parsed : {};
|
|
756
|
+
const data = isRecord(record.data) ? record.data : null;
|
|
757
|
+
const event = data && isRecord(data.event) ? data.event : isRecord(record.event) ? record.event : data || record;
|
|
758
|
+
void onFeishuMessage(agent, event);
|
|
759
|
+
}
|
|
760
|
+
catch { /* not an event */ }
|
|
761
|
+
});
|
|
762
|
+
child.on("exit", (code) => log(`事件源退出 agent=${agent.name} code=${code}`));
|
|
763
|
+
};
|
|
764
|
+
if (eventCommand) {
|
|
765
|
+
const parts = eventCommand.split(" ");
|
|
766
|
+
startChild(agents[0], parts[0], parts.slice(1));
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
if (eventFile) {
|
|
770
|
+
log("文件事件源(测试):", eventFile);
|
|
771
|
+
let offset = 0;
|
|
772
|
+
node_fs_1.default.writeFileSync(eventFile, node_fs_1.default.existsSync(eventFile) ? node_fs_1.default.readFileSync(eventFile) : "");
|
|
773
|
+
const fileTimer = setInterval(() => {
|
|
774
|
+
try {
|
|
775
|
+
const content = node_fs_1.default.readFileSync(eventFile, "utf8");
|
|
776
|
+
if (content.length <= offset)
|
|
777
|
+
return;
|
|
778
|
+
const fresh = content.slice(offset);
|
|
779
|
+
offset = content.length;
|
|
780
|
+
for (const line of fresh.split("\n").filter(Boolean)) {
|
|
781
|
+
try {
|
|
782
|
+
void onFeishuMessage(agents[0], JSON.parse(line));
|
|
783
|
+
}
|
|
784
|
+
catch { /* partial line */ }
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
catch { /* retry */ }
|
|
788
|
+
}, 500);
|
|
789
|
+
eventSourceStop = () => clearInterval(fileTimer);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
const profileAgents = agents.filter((agent) => agent.feishuProfile);
|
|
793
|
+
if (!profileAgents.length) {
|
|
794
|
+
log("未配置 channel 事件源,入站不启动");
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
const channels = [];
|
|
798
|
+
const channelOwners = new Map();
|
|
799
|
+
const activeChannels = new Map();
|
|
800
|
+
const reconnectFns = new Map();
|
|
801
|
+
const retryTimers = new Set();
|
|
802
|
+
let droughtTimer = null;
|
|
803
|
+
const droughtMaintenance = new Map();
|
|
804
|
+
const eventSourceStartedMs = Date.now();
|
|
805
|
+
let fataling = false;
|
|
806
|
+
const disconnectSafely = (channel) => {
|
|
807
|
+
try {
|
|
808
|
+
return Promise.resolve(channel.disconnect?.());
|
|
809
|
+
}
|
|
810
|
+
catch (error) {
|
|
811
|
+
return Promise.reject(error);
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
const disconnectWithinBound = (channel) => new Promise((resolve, reject) => {
|
|
815
|
+
const timer = setTimeout(() => reject(new Error("channel disconnect timeout")), channelDisconnectTimeoutMs);
|
|
816
|
+
disconnectSafely(channel).then(() => { clearTimeout(timer); resolve(); }, (error) => { clearTimeout(timer); reject(error); });
|
|
817
|
+
});
|
|
818
|
+
const connectCandidate = async (agent, activateImmediately = false) => {
|
|
819
|
+
const { createLarkChannel } = channelPackage ?? channelSdk;
|
|
820
|
+
const channel = createLarkChannel({
|
|
821
|
+
appId: agent.feishuAppId,
|
|
822
|
+
appSecret: agent.feishuAppSecret,
|
|
823
|
+
domain: agent.feishuDomain || "https://open.feishu.cn",
|
|
824
|
+
source: "larkin",
|
|
825
|
+
policy: { dmMode: "open", requireMention: false, respondToMentionAll: true },
|
|
826
|
+
handshakeTimeoutMs: 15_000,
|
|
827
|
+
keepalive: { enabled: true, intervalMs: 15_000 },
|
|
828
|
+
includeRawEvent: true,
|
|
829
|
+
});
|
|
830
|
+
const handlers = channelBusiness.handlers(agent, channel);
|
|
831
|
+
let active = activateImmediately;
|
|
832
|
+
const queuedMessages = [];
|
|
833
|
+
const queuedComments = [];
|
|
834
|
+
channel.on({ ...handlers,
|
|
835
|
+
message: (message) => { if (active)
|
|
836
|
+
handlers.message(message);
|
|
837
|
+
else
|
|
838
|
+
queuedMessages.push(message); },
|
|
839
|
+
comment: async (comment) => { if (active)
|
|
840
|
+
await handlers.comment(comment);
|
|
841
|
+
else
|
|
842
|
+
queuedComments.push(comment); },
|
|
843
|
+
cardAction: (event) => active ? handlers.cardAction(event)
|
|
844
|
+
: Promise.resolve({ toast: { type: "info", content: "Agent 配置切换中,请稍后重试。" } }),
|
|
845
|
+
});
|
|
846
|
+
channelBusiness.registerReadReceipts(agent, channel.dispatcher);
|
|
847
|
+
try {
|
|
848
|
+
await channel.connect();
|
|
849
|
+
if (!channel.botIdentity?.openId)
|
|
850
|
+
throw new Error(`Agent ${agent.agentId} channel 已连接但 bot identity 不可用`);
|
|
851
|
+
await channelBusiness.connected(agent, channel);
|
|
852
|
+
await replayPendingDocumentComments(agent, channel);
|
|
853
|
+
let activated = false;
|
|
854
|
+
const activate = () => {
|
|
855
|
+
if (activated)
|
|
856
|
+
return;
|
|
857
|
+
activated = true;
|
|
858
|
+
active = true;
|
|
859
|
+
channelBusiness.registerCardActions(agent, channel.dispatcher);
|
|
860
|
+
for (const message of queuedMessages.splice(0))
|
|
861
|
+
handlers.message(message);
|
|
862
|
+
for (const comment of queuedComments.splice(0))
|
|
863
|
+
void handlers.comment(comment);
|
|
864
|
+
};
|
|
865
|
+
if (activateImmediately)
|
|
866
|
+
activate();
|
|
867
|
+
return { channel, activate };
|
|
868
|
+
}
|
|
869
|
+
catch (error) {
|
|
870
|
+
await disconnectWithinBound(channel).catch(() => { });
|
|
871
|
+
throw error;
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
let channelStopPromise = null;
|
|
875
|
+
eventSourceStop = () => {
|
|
876
|
+
if (channelStopPromise)
|
|
877
|
+
return channelStopPromise;
|
|
878
|
+
shuttingDown = true;
|
|
879
|
+
if (droughtTimer)
|
|
880
|
+
clearInterval(droughtTimer);
|
|
881
|
+
for (const timer of retryTimers)
|
|
882
|
+
clearTimeout(timer);
|
|
883
|
+
retryTimers.clear();
|
|
884
|
+
log(`disconnect ${channels.length} 条事件连接…`);
|
|
885
|
+
const closingChannels = [...channels];
|
|
886
|
+
channelStopPromise = Promise.allSettled(closingChannels.map(disconnectWithinBound)).then((results) => {
|
|
887
|
+
let failures = 0;
|
|
888
|
+
for (let index = 0; index < results.length; index += 1) {
|
|
889
|
+
const result = results[index];
|
|
890
|
+
if (result.status === "fulfilled")
|
|
891
|
+
continue;
|
|
892
|
+
failures += 1;
|
|
893
|
+
const agent = channelOwners.get(closingChannels[index]);
|
|
894
|
+
const outcome = /timeout/i.test(errorMessage(result.reason)) ? "超时" : "失败";
|
|
895
|
+
const message = `channel disconnect ${outcome}${agent ? ` agent=${agent.name}` : ""}`;
|
|
896
|
+
if (agent)
|
|
897
|
+
hostState.recordStatusError(agent, message);
|
|
898
|
+
log(message);
|
|
899
|
+
}
|
|
900
|
+
if (failures === 0)
|
|
901
|
+
log("已断开事件连接");
|
|
902
|
+
else
|
|
903
|
+
log(`事件连接关闭完成:成功 ${results.length - failures},失败 ${failures}`);
|
|
904
|
+
});
|
|
905
|
+
return channelStopPromise;
|
|
906
|
+
};
|
|
907
|
+
const failEventSource = (agent, error) => {
|
|
908
|
+
if (fataling || shuttingDown)
|
|
909
|
+
return;
|
|
910
|
+
fataling = true;
|
|
911
|
+
for (const timer of retryTimers)
|
|
912
|
+
clearTimeout(timer);
|
|
913
|
+
retryTimers.clear();
|
|
914
|
+
const authish = /secret|invalid|bot identity|10014|20005|unauthorized|forbidden/i.test(errorMessage(error));
|
|
915
|
+
const message = `channel 入站不可用 agent=${agent.name}: ${authish ? "认证失败" : "连接重试耗尽"}`;
|
|
916
|
+
hostState.recordStatusError(agent, message);
|
|
917
|
+
log(`${message};整进程退出,请修复凭证/连接后重启`);
|
|
918
|
+
requestOrderedShutdown(message, 1);
|
|
919
|
+
};
|
|
920
|
+
for (const agent of profileAgents) {
|
|
921
|
+
let retries = 0;
|
|
922
|
+
const tryChannel = () => {
|
|
923
|
+
let channel;
|
|
924
|
+
const removeChannel = () => {
|
|
925
|
+
const index = channels.indexOf(channel);
|
|
926
|
+
if (index >= 0)
|
|
927
|
+
channels.splice(index, 1);
|
|
928
|
+
channelOwners.delete(channel);
|
|
929
|
+
if (activeChannels.get(agent.agentId) === channel)
|
|
930
|
+
activeChannels.delete(agent.agentId);
|
|
931
|
+
};
|
|
932
|
+
try {
|
|
933
|
+
channel = startChannelSource(agent, {
|
|
934
|
+
onFatal: (error) => failEventSource(agent, error),
|
|
935
|
+
onConnectFail: (error) => {
|
|
936
|
+
if (shuttingDown || fataling)
|
|
937
|
+
return;
|
|
938
|
+
const authish = /secret|invalid|bot identity|10014|20005|unauthorized|forbidden/i.test(errorMessage(error));
|
|
939
|
+
if (authish || retries >= 5) {
|
|
940
|
+
failEventSource(agent, error);
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
const delay = Math.min(60, 5 * 2 ** retries++);
|
|
944
|
+
disconnectWithinBound(channel).then(() => {
|
|
945
|
+
if (shuttingDown || fataling)
|
|
946
|
+
return;
|
|
947
|
+
removeChannel();
|
|
948
|
+
hostState.recordStatusError(agent, `channel.connect 失败,${delay}s 后重试`);
|
|
949
|
+
log(`channel.connect ${delay}s 后重试 agent=${agent.name}(第 ${retries} 次)`);
|
|
950
|
+
const timer = setTimeout(() => { retryTimers.delete(timer); tryChannel(); }, delay * 1_000);
|
|
951
|
+
retryTimers.add(timer);
|
|
952
|
+
}).catch((disconnectError) => failEventSource(agent, disconnectError));
|
|
953
|
+
},
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
catch (error) {
|
|
957
|
+
failEventSource(agent, error);
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
channels.push(channel);
|
|
961
|
+
channelOwners.set(channel, agent);
|
|
962
|
+
activeChannels.set(agent.agentId, channel);
|
|
963
|
+
};
|
|
964
|
+
reconnectFns.set(agent.agentId, tryChannel);
|
|
965
|
+
tryChannel();
|
|
966
|
+
if (fataling)
|
|
967
|
+
break;
|
|
968
|
+
}
|
|
969
|
+
hotUpsert = async (candidate) => {
|
|
970
|
+
if (shuttingDown || fataling)
|
|
971
|
+
throw new Error("HostShell 正在关闭或 channel 不可用");
|
|
972
|
+
const index = agents.findIndex((agent) => agent.agentId === candidate.agentId);
|
|
973
|
+
const previous = index >= 0 ? agents[index] : null;
|
|
974
|
+
const unchanged = previous && agentConfigSignature(previous) === agentConfigSignature(candidate);
|
|
975
|
+
if (unchanged && activeChannels.has(candidate.agentId))
|
|
976
|
+
return "unchanged";
|
|
977
|
+
// Runtime replacement is target-only. The old channel remains connected until the
|
|
978
|
+
// candidate Runtime and channel are both ready, so unrelated Agents are untouched.
|
|
979
|
+
hydrateBotIdentity(candidate);
|
|
980
|
+
const runtimeCandidate = { ...candidate,
|
|
981
|
+
sessionId: agentStates.get(candidate.agentId)?.state.sessions[candidate.runtime] || null };
|
|
982
|
+
const staged = previous && runtimeHost.stage ? await runtimeHost.stage(runtimeCandidate) : null;
|
|
983
|
+
const candidateReadiness = staged?.readiness ?? (runtimeHost.probe ? await runtimeHost.probe(runtimeCandidate)
|
|
984
|
+
: { runtime: candidate.runtime, state: "ready" });
|
|
985
|
+
if (candidateReadiness.state !== "ready")
|
|
986
|
+
throw new runtime_readiness_js_1.RuntimePrerequisiteError(candidateReadiness);
|
|
987
|
+
initializeAgentState(candidate);
|
|
988
|
+
prepareAgentState(candidate);
|
|
989
|
+
if (!staged) {
|
|
990
|
+
if (previous)
|
|
991
|
+
await runtimeHost.stop(previous.agentId, "target agent hot update");
|
|
992
|
+
try {
|
|
993
|
+
await runtimeHost.start([runtimeCandidate]);
|
|
994
|
+
}
|
|
995
|
+
catch (error) {
|
|
996
|
+
if (previous)
|
|
997
|
+
await runtimeHost.start([{ ...previous,
|
|
998
|
+
sessionId: agentStates.get(previous.agentId)?.state.sessions[previous.runtime] || null }]).catch(() => { });
|
|
999
|
+
throw error;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
let nextConnection;
|
|
1003
|
+
try {
|
|
1004
|
+
nextConnection = await connectCandidate(candidate);
|
|
1005
|
+
}
|
|
1006
|
+
catch (error) {
|
|
1007
|
+
if (staged)
|
|
1008
|
+
await staged.rollback("hot attach channel rollback");
|
|
1009
|
+
else
|
|
1010
|
+
await runtimeHost.stop(candidate.agentId, "hot attach channel rollback");
|
|
1011
|
+
if (previous && !staged)
|
|
1012
|
+
await runtimeHost.start([{
|
|
1013
|
+
...previous,
|
|
1014
|
+
sessionId: agentStates.get(previous.agentId)?.state.sessions[previous.runtime] || null,
|
|
1015
|
+
}]).catch(() => { });
|
|
1016
|
+
throw error;
|
|
1017
|
+
}
|
|
1018
|
+
const nextChannel = nextConnection.channel;
|
|
1019
|
+
const oldChannel = activeChannels.get(candidate.agentId);
|
|
1020
|
+
if (oldChannel) {
|
|
1021
|
+
try {
|
|
1022
|
+
await disconnectWithinBound(oldChannel);
|
|
1023
|
+
}
|
|
1024
|
+
catch (error) {
|
|
1025
|
+
await disconnectWithinBound(nextChannel).catch(() => { });
|
|
1026
|
+
if (staged)
|
|
1027
|
+
await staged.rollback("hot update old channel rollback");
|
|
1028
|
+
else
|
|
1029
|
+
await runtimeHost.stop(candidate.agentId, "hot update old channel rollback");
|
|
1030
|
+
if (previous && !staged)
|
|
1031
|
+
await runtimeHost.start([{
|
|
1032
|
+
...previous,
|
|
1033
|
+
sessionId: agentStates.get(previous.agentId)?.state.sessions[previous.runtime] || null,
|
|
1034
|
+
}]).catch(() => { });
|
|
1035
|
+
throw new Error(`旧 Agent channel 无法安全断开:${errorMessage(error)}`);
|
|
1036
|
+
}
|
|
1037
|
+
const oldIndex = channels.indexOf(oldChannel);
|
|
1038
|
+
if (oldIndex >= 0)
|
|
1039
|
+
channels.splice(oldIndex, 1);
|
|
1040
|
+
channelOwners.delete(oldChannel);
|
|
1041
|
+
}
|
|
1042
|
+
if (staged) {
|
|
1043
|
+
try {
|
|
1044
|
+
await staged.commit();
|
|
1045
|
+
}
|
|
1046
|
+
catch (error) {
|
|
1047
|
+
await disconnectWithinBound(nextChannel).catch(() => { });
|
|
1048
|
+
throw error;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
if (index >= 0)
|
|
1052
|
+
agents[index] = candidate;
|
|
1053
|
+
else
|
|
1054
|
+
agents.push(candidate);
|
|
1055
|
+
if (!profileAgents.some((agent) => agent.agentId === candidate.agentId))
|
|
1056
|
+
profileAgents.push(candidate);
|
|
1057
|
+
else {
|
|
1058
|
+
const profileIndex = profileAgents.findIndex((agent) => agent.agentId === candidate.agentId);
|
|
1059
|
+
profileAgents[profileIndex] = candidate;
|
|
1060
|
+
}
|
|
1061
|
+
channels.push(nextChannel);
|
|
1062
|
+
channelOwners.set(nextChannel, candidate);
|
|
1063
|
+
activeChannels.set(candidate.agentId, nextChannel);
|
|
1064
|
+
interactionChannels.set(candidate.agentId, nextChannel);
|
|
1065
|
+
nextConnection.activate();
|
|
1066
|
+
reconnectFns.set(candidate.agentId, () => {
|
|
1067
|
+
void connectCandidate(candidate, true).then(({ channel }) => {
|
|
1068
|
+
channels.push(channel);
|
|
1069
|
+
channelOwners.set(channel, candidate);
|
|
1070
|
+
activeChannels.set(candidate.agentId, channel);
|
|
1071
|
+
interactionChannels.set(candidate.agentId, channel);
|
|
1072
|
+
}).catch((error) => failEventSource(candidate, error));
|
|
1073
|
+
});
|
|
1074
|
+
interaction.syncAgent(candidate);
|
|
1075
|
+
await interaction.flushPending(candidate);
|
|
1076
|
+
reminder.pushSnapshot(candidate, "hot attach", true);
|
|
1077
|
+
await reminder.redeliverUnread(candidate);
|
|
1078
|
+
try {
|
|
1079
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(larkinHome, "daemon-status.json"), JSON.stringify({
|
|
1080
|
+
...(0, process_inspect_cjs_1.currentProcessMetadata)((0, internal_command_js_1.processCommandToken)("daemon", "app/runtime-process.mjs")), pid: process.pid, startedAt: daemonStartedAt,
|
|
1081
|
+
agents: agents.map((agent) => agent.agentId),
|
|
1082
|
+
}, null, 2), { mode: 0o600 });
|
|
1083
|
+
}
|
|
1084
|
+
catch (error) {
|
|
1085
|
+
log(`daemon ownership 更新失败: ${errorMessage(error)}`);
|
|
1086
|
+
}
|
|
1087
|
+
return previous ? "updated" : "added";
|
|
1088
|
+
};
|
|
1089
|
+
// 预防性重连 watchdog:连接自称健康但长期零入站事件 → 疑似僵尸连接(ws 活着、飞书事件路由死了,
|
|
1090
|
+
// 2026-07-17 实测;传输层 keepalive 探不出来)。主动断开重建无害——飞书会补投窗口内事件。
|
|
1091
|
+
// 默认 10 分钟;LARKIN_INBOUND_DROUGHT_SEC 可调(集成测试用小值),设 0 关闭。
|
|
1092
|
+
const droughtSecRaw = env.LARKIN_INBOUND_DROUGHT_SEC;
|
|
1093
|
+
const droughtSec = droughtSecRaw === undefined || droughtSecRaw === "" ? 600 : Math.max(0, Number(droughtSecRaw) || 0);
|
|
1094
|
+
if (droughtSec > 0) {
|
|
1095
|
+
const attemptDroughtMaintenance = (agent, channel, maintenance) => {
|
|
1096
|
+
if (shuttingDown || fataling || maintenance.inFlight || maintenance.closed)
|
|
1097
|
+
return;
|
|
1098
|
+
if (activeChannels.get(agent.agentId) !== channel)
|
|
1099
|
+
return;
|
|
1100
|
+
const latestInboundAt = Date.parse(String(hostState.readStatus(agent).inboundVerifiedAt || ""));
|
|
1101
|
+
if (Number.isFinite(latestInboundAt) && (maintenance.baselineInboundAt === null || latestInboundAt > maintenance.baselineInboundAt)) {
|
|
1102
|
+
droughtMaintenance.delete(agent.agentId);
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
maintenance.inFlight = true;
|
|
1106
|
+
log(`入站静默 ≥${droughtSec}s(连接在线但零事件),本静默周期执行一次预防性重连 agent=${agent.name}`);
|
|
1107
|
+
disconnectWithinBound(channel).then(() => {
|
|
1108
|
+
maintenance.inFlight = false;
|
|
1109
|
+
maintenance.closed = true;
|
|
1110
|
+
if (shuttingDown || fataling)
|
|
1111
|
+
return;
|
|
1112
|
+
if (activeChannels.get(agent.agentId) !== channel)
|
|
1113
|
+
return;
|
|
1114
|
+
activeChannels.delete(agent.agentId);
|
|
1115
|
+
channelOwners.delete(channel);
|
|
1116
|
+
const index = channels.indexOf(channel);
|
|
1117
|
+
if (index >= 0)
|
|
1118
|
+
channels.splice(index, 1);
|
|
1119
|
+
const droughtReconnectAt = new Date().toISOString();
|
|
1120
|
+
// 成功维护是中性活动;只有 disconnect 真失败才记录 recentErrors。
|
|
1121
|
+
hostState.appendStatusLog(agent, "activityLog", {
|
|
1122
|
+
at: droughtReconnectAt,
|
|
1123
|
+
state: "online",
|
|
1124
|
+
detail: `入站静默 ≥${droughtSec}s,本静默周期已完成一次预防性重连`,
|
|
1125
|
+
}, 80);
|
|
1126
|
+
hostState.updateStatus(agent, { droughtReconnectAt, droughtReconnectAbandonedAt: null });
|
|
1127
|
+
reconnectFns.get(agent.agentId)?.();
|
|
1128
|
+
}).catch((error) => {
|
|
1129
|
+
maintenance.inFlight = false;
|
|
1130
|
+
if (shuttingDown || fataling)
|
|
1131
|
+
return;
|
|
1132
|
+
maintenance.failures += 1;
|
|
1133
|
+
const timeout = /timeout/i.test(errorMessage(error));
|
|
1134
|
+
const reason = timeout ? "超时" : "被 SDK 拒绝";
|
|
1135
|
+
if (!maintenance.failureRecorded) {
|
|
1136
|
+
maintenance.failureRecorded = true;
|
|
1137
|
+
hostState.recordStatusError(agent, `预防性重连未执行:旧 channel 断开失败(${reason}),将有限重试一次`);
|
|
1138
|
+
}
|
|
1139
|
+
if (maintenance.failures >= 2) {
|
|
1140
|
+
maintenance.closed = true;
|
|
1141
|
+
const droughtReconnectAbandonedAt = new Date().toISOString();
|
|
1142
|
+
hostState.updateStatus(agent, { droughtReconnectAbandonedAt });
|
|
1143
|
+
log(`预防性重连停止:旧 channel 连续 2 次断开失败(${reason}),保留原 channel 等待真实入站后开启新周期 agent=${agent.name}`);
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
maintenance.retryScheduled = true;
|
|
1147
|
+
const retryDelayMs = Math.min(60_000, Math.max(1_000, droughtSec * 1_000));
|
|
1148
|
+
const timer = setTimeout(() => {
|
|
1149
|
+
retryTimers.delete(timer);
|
|
1150
|
+
maintenance.retryScheduled = false;
|
|
1151
|
+
attemptDroughtMaintenance(agent, channel, maintenance);
|
|
1152
|
+
}, retryDelayMs);
|
|
1153
|
+
retryTimers.add(timer);
|
|
1154
|
+
log(`预防性重连未执行:旧 channel 断开失败(${reason}),保留原 channel 并有限重试一次 agent=${agent.name}`);
|
|
1155
|
+
});
|
|
1156
|
+
};
|
|
1157
|
+
droughtTimer = setInterval(() => {
|
|
1158
|
+
if (shuttingDown || fataling)
|
|
1159
|
+
return;
|
|
1160
|
+
for (const agent of profileAgents) {
|
|
1161
|
+
const channel = activeChannels.get(agent.agentId);
|
|
1162
|
+
if (!channel)
|
|
1163
|
+
continue;
|
|
1164
|
+
const now = new Date();
|
|
1165
|
+
const status = hostState.readStatus(agent);
|
|
1166
|
+
const inboundAt = Date.parse(String(status.inboundVerifiedAt || ""));
|
|
1167
|
+
let maintenance = droughtMaintenance.get(agent.agentId);
|
|
1168
|
+
if (maintenance && Number.isFinite(inboundAt) && (maintenance.baselineInboundAt === null || inboundAt > maintenance.baselineInboundAt)) {
|
|
1169
|
+
droughtMaintenance.delete(agent.agentId);
|
|
1170
|
+
maintenance = undefined;
|
|
1171
|
+
}
|
|
1172
|
+
if (maintenance?.inFlight || maintenance?.retryScheduled || maintenance?.closed)
|
|
1173
|
+
continue;
|
|
1174
|
+
if (!(0, host_business_state_js_1.shouldPreventiveReconnect)(status, now, droughtSec * 1_000, eventSourceStartedMs))
|
|
1175
|
+
continue;
|
|
1176
|
+
if (!maintenance) {
|
|
1177
|
+
maintenance = {
|
|
1178
|
+
baselineInboundAt: Number.isFinite(inboundAt) ? inboundAt : null,
|
|
1179
|
+
failures: 0,
|
|
1180
|
+
inFlight: false,
|
|
1181
|
+
retryScheduled: false,
|
|
1182
|
+
closed: false,
|
|
1183
|
+
failureRecorded: false,
|
|
1184
|
+
};
|
|
1185
|
+
droughtMaintenance.set(agent.agentId, maintenance);
|
|
1186
|
+
}
|
|
1187
|
+
attemptDroughtMaintenance(agent, channel, maintenance);
|
|
1188
|
+
}
|
|
1189
|
+
}, Math.max(1_000, Math.min(60_000, droughtSec * 500)));
|
|
1190
|
+
droughtTimer.unref?.();
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
const onRuntimeEvent = (message) => {
|
|
1194
|
+
const agent = agents.find((candidate) => candidate.agentId === message.agentId);
|
|
1195
|
+
if (!agent)
|
|
1196
|
+
return;
|
|
1197
|
+
if (message.type === "agent-status") {
|
|
1198
|
+
log("agent:status", message.agentId, message.status);
|
|
1199
|
+
if (message.status === "error" || message.status === "inactive")
|
|
1200
|
+
hostState.updateStatus(agent, {
|
|
1201
|
+
runtimeReadiness: message.readiness?.state && message.readiness.state !== "ready" ? message.readiness : {
|
|
1202
|
+
runtime: agent.runtime,
|
|
1203
|
+
state: message.status === "error" ? "incompatible" : "missing",
|
|
1204
|
+
reason: message.status === "error" ? message.error || "Runtime entered an error state" : "Runtime is inactive",
|
|
1205
|
+
},
|
|
1206
|
+
});
|
|
1207
|
+
else if (message.readiness)
|
|
1208
|
+
hostState.updateStatus(agent, { runtimeReadiness: message.readiness });
|
|
1209
|
+
else if (message.status === "active")
|
|
1210
|
+
hostState.updateStatus(agent, { runtimeReadiness: {
|
|
1211
|
+
runtime: agent.runtime, state: "ready",
|
|
1212
|
+
} });
|
|
1213
|
+
if (message.status === "active") {
|
|
1214
|
+
const redeliveryTimer = setTimeout(() => {
|
|
1215
|
+
void reminder.redeliverUnread(agent).catch((error) => log("启动补投失败", errorMessage(error)));
|
|
1216
|
+
}, 5_000);
|
|
1217
|
+
redeliveryTimer.unref?.();
|
|
1218
|
+
}
|
|
1219
|
+
if (message.status === "error" && message.error)
|
|
1220
|
+
hostState.recordStatusError(agent, message.error);
|
|
1221
|
+
if (message.status === "error" || message.status === "inactive") {
|
|
1222
|
+
processingEyes.clear(agent, `agent-status:${message.status}`);
|
|
1223
|
+
}
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
if (message.type === "activity") {
|
|
1227
|
+
if (message.isHeartbeat)
|
|
1228
|
+
return;
|
|
1229
|
+
const frame = { agentId: message.agentId, activity: message.activity,
|
|
1230
|
+
activityKind: message.activityKind, detailKind: message.detailKind, isHeartbeat: message.isHeartbeat };
|
|
1231
|
+
const patch = (0, host_business_state_js_1.projectActivityStatus)(hostState.readStatus(agent), frame, agent.runtime, agentStates.get(agent.agentId)?.state.sessions[agent.runtime] || null);
|
|
1232
|
+
if (patch)
|
|
1233
|
+
hostState.updateStatus(agent, patch);
|
|
1234
|
+
processingEyes.observeActivity(agent, message.activity);
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
if (message.type === "delivery") {
|
|
1238
|
+
hostState.appendStatusLog(agent, "deliverLog", {
|
|
1239
|
+
at: new Date().toISOString(), deliveryId: message.deliveryId, messageId: message.messageId,
|
|
1240
|
+
status: message.status, ...(message.reason ? { reason: (0, host_business_state_js_1.safeConversationExcerpt)(message.reason, 120) } : {}),
|
|
1241
|
+
}, 80);
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
if (message.type === "session") {
|
|
1245
|
+
const record = agentStates.get(agent.agentId);
|
|
1246
|
+
if (record && record.state.sessions[message.runtime] !== message.sessionId) {
|
|
1247
|
+
record.state.sessions[message.runtime] = message.sessionId;
|
|
1248
|
+
saveAgentState(record);
|
|
1249
|
+
log(`持久化 agent=${agent.name} runtime=${message.runtime} sessionId=${message.sessionId}`);
|
|
1250
|
+
}
|
|
1251
|
+
hostState.updateStatus(agent, (0, host_business_state_js_1.projectSessionStatus)(hostState.readStatus(agent), message.runtime, message.sessionId, message.launchId, new Date(), {
|
|
1252
|
+
...(message.model ? { model: message.model } : {}),
|
|
1253
|
+
...(message.reasoningEffort ? { reasoningEffort: message.reasoningEffort } : {}),
|
|
1254
|
+
}));
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
if (message.type === "runtime" && message.event.type === "input-error") {
|
|
1258
|
+
if (message.event.errorCategory === "auth") {
|
|
1259
|
+
const readiness = (0, runtime_readiness_js_1.providerAuthenticationFailureReadiness)(agent.runtime, message.event.upstream?.provider);
|
|
1260
|
+
hostState.recordStatusError(agent, `auth: ${readiness.reason}; ${readiness.nextAction}`);
|
|
1261
|
+
}
|
|
1262
|
+
else {
|
|
1263
|
+
hostState.recordStatusError(agent, `${message.event.errorCategory || "provider"}: ${message.event.message}${message.event.nextAction ? `; ${message.event.nextAction}` : ""}`);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
};
|
|
1267
|
+
let shutdownPromise = null;
|
|
1268
|
+
let orderedExitDispatched = false;
|
|
1269
|
+
const dispatchOrderedExit = (exitCode) => {
|
|
1270
|
+
if (orderedExitDispatched || !onOrderedShutdownComplete)
|
|
1271
|
+
return;
|
|
1272
|
+
orderedExitDispatched = true;
|
|
1273
|
+
onOrderedShutdownComplete(exitCode);
|
|
1274
|
+
};
|
|
1275
|
+
const shutdown = (reason = "shutdown") => {
|
|
1276
|
+
if (shutdownPromise)
|
|
1277
|
+
return shutdownPromise;
|
|
1278
|
+
shutdownPromise = (async () => {
|
|
1279
|
+
if (eventSourceStartTimer)
|
|
1280
|
+
clearTimeout(eventSourceStartTimer);
|
|
1281
|
+
eventSourceStartTimer = null;
|
|
1282
|
+
await Promise.resolve(eventSourceStop());
|
|
1283
|
+
reminder.stopSync();
|
|
1284
|
+
interaction.stopSync();
|
|
1285
|
+
await runtimeHost.shutdown(reason);
|
|
1286
|
+
})();
|
|
1287
|
+
return shutdownPromise;
|
|
1288
|
+
};
|
|
1289
|
+
requestOrderedShutdown = (reason, exitCode) => {
|
|
1290
|
+
if (exitCode !== undefined)
|
|
1291
|
+
process.exitCode = exitCode;
|
|
1292
|
+
const currentExitCode = typeof process.exitCode === "number" ? process.exitCode : Number(process.exitCode) || 0;
|
|
1293
|
+
const requestedExitCode = exitCode ?? currentExitCode;
|
|
1294
|
+
return shutdown(reason).then(() => dispatchOrderedExit(requestedExitCode)).catch((error) => {
|
|
1295
|
+
process.exitCode = 1;
|
|
1296
|
+
log(`shutdown 失败: ${errorMessage(error)}`);
|
|
1297
|
+
dispatchOrderedExit(1);
|
|
1298
|
+
});
|
|
1299
|
+
};
|
|
1300
|
+
return {
|
|
1301
|
+
agents,
|
|
1302
|
+
serverId,
|
|
1303
|
+
log,
|
|
1304
|
+
resumeSession(agent, runtime) { return agentStates.get(agent.agentId)?.state.sessions[runtime] || null; },
|
|
1305
|
+
async ingest(agentId, event, options) {
|
|
1306
|
+
const agent = agents.find((candidate) => candidate.agentId === agentId);
|
|
1307
|
+
if (!agent)
|
|
1308
|
+
throw Object.assign(new Error(`未知 Agent: ${agentId}`), { code: "unknown_agent" });
|
|
1309
|
+
await onFeishuMessage(agent, event, options);
|
|
1310
|
+
},
|
|
1311
|
+
async upsertAgent(candidate) {
|
|
1312
|
+
const validated = loadAgents({ ...env, LARKIN_AGENTS_CONFIG: JSON.stringify([candidate]) }, false, larkinHome, reconcileAgentWorkspaceImpl)[0];
|
|
1313
|
+
if (!hotUpsert)
|
|
1314
|
+
throw new Error("Agent control plane 尚未就绪");
|
|
1315
|
+
if (runtimeHost.isBusy?.(validated.agentId))
|
|
1316
|
+
throw new Error(`Agent ${validated.agentId} 正在执行 turn,拒绝中断;请在 idle 后重试`);
|
|
1317
|
+
return hotUpsert(validated);
|
|
1318
|
+
},
|
|
1319
|
+
async resetSession(agentId, waitReadyMs = 30_000) {
|
|
1320
|
+
const agent = agents.find((candidate) => candidate.agentId === agentId);
|
|
1321
|
+
if (!agent)
|
|
1322
|
+
throw Object.assign(new Error(`未知 Agent: ${agentId}`), { code: "unknown_agent" });
|
|
1323
|
+
if (!runtimeHost.resetSession)
|
|
1324
|
+
throw Object.assign(new Error("Runtime session reset 尚未就绪"), { code: "runtime_unavailable" });
|
|
1325
|
+
const connectionState = (status = hostState.readStatus(agent)) => {
|
|
1326
|
+
const connectedAt = Date.parse(String(status.connectedAt || ""));
|
|
1327
|
+
const channelConnected = Number.isFinite(connectedAt) && connectedAt >= Date.parse(daemonStartedAt) - 1000;
|
|
1328
|
+
return { channelConnected, reconnecting: (0, agent_readiness_js_1.isChannelReconnecting)(status) };
|
|
1329
|
+
};
|
|
1330
|
+
const readinessProjection = () => {
|
|
1331
|
+
const status = hostState.readStatus(agent);
|
|
1332
|
+
const turns = status.session && typeof status.session === "object"
|
|
1333
|
+
? Math.max(0, Number(status.session.turns) || 0) : 0;
|
|
1334
|
+
const runtimeReady = Boolean(status.runtimeReadiness && typeof status.runtimeReadiness === "object"
|
|
1335
|
+
&& status.runtimeReadiness.state === "ready");
|
|
1336
|
+
const pendingCount = stateStore(agent).withInboxTransaction(() => stateStore(agent).readNdjson("inbox").length);
|
|
1337
|
+
return { turns, runtimeReady, ...connectionState(status), pendingCount };
|
|
1338
|
+
};
|
|
1339
|
+
const initialProjection = readinessProjection();
|
|
1340
|
+
if (!initialProjection.channelConnected)
|
|
1341
|
+
throw Object.assign(new Error(`Agent ${agentId} channel is not connected`), { code: "channel_unavailable", ...initialProjection });
|
|
1342
|
+
if (initialProjection.reconnecting)
|
|
1343
|
+
throw Object.assign(new Error(`Agent ${agentId} channel is reconnecting`), { code: "channel_reconnecting", ...initialProjection });
|
|
1344
|
+
let reset;
|
|
1345
|
+
try {
|
|
1346
|
+
reset = await runtimeHost.resetSession(agentId);
|
|
1347
|
+
}
|
|
1348
|
+
catch (error) {
|
|
1349
|
+
throw Object.assign(error instanceof Error ? error : new Error(String(error)), readinessProjection());
|
|
1350
|
+
}
|
|
1351
|
+
const record = agentStates.get(agentId);
|
|
1352
|
+
try {
|
|
1353
|
+
if (record) {
|
|
1354
|
+
if (reset.sessionId)
|
|
1355
|
+
record.state.sessions[agent.runtime] = reset.sessionId;
|
|
1356
|
+
else
|
|
1357
|
+
delete record.state.sessions[agent.runtime];
|
|
1358
|
+
record.store.writeJson("agentState", record.state);
|
|
1359
|
+
}
|
|
1360
|
+
if (!reset.sessionId)
|
|
1361
|
+
hostState.updateStatus(agent, {
|
|
1362
|
+
session: { runtime: agent.runtime, id: null, launchId: null, startedAt: new Date().toISOString(),
|
|
1363
|
+
lastSeenAt: null, lastTurnAt: null, turns: 0 }, runtimeReadiness: { runtime: agent.runtime, state: "ready" },
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
catch (error) {
|
|
1367
|
+
const projection = readinessProjection();
|
|
1368
|
+
return { resetCommitted: true, generationChanged: reset.generationChanged, sessionChanged: reset.sessionChanged,
|
|
1369
|
+
...projection,
|
|
1370
|
+
readyForFreshScenario: false, inboundObserved: false, code: "state_persistence_failed",
|
|
1371
|
+
error: error instanceof Error ? error.message : String(error) };
|
|
1372
|
+
}
|
|
1373
|
+
const deadline = Date.now() + Math.max(0, waitReadyMs);
|
|
1374
|
+
let projection = readinessProjection();
|
|
1375
|
+
do {
|
|
1376
|
+
projection = readinessProjection();
|
|
1377
|
+
if (projection.turns === 0 && projection.runtimeReady && projection.channelConnected && !projection.reconnecting && projection.pendingCount === 0) {
|
|
1378
|
+
return { resetCommitted: true, generationChanged: reset.generationChanged, sessionChanged: reset.sessionChanged,
|
|
1379
|
+
...projection, readyForFreshScenario: true, inboundObserved: false };
|
|
1380
|
+
}
|
|
1381
|
+
if (Date.now() >= deadline)
|
|
1382
|
+
break;
|
|
1383
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(25, Math.max(1, deadline - Date.now()))));
|
|
1384
|
+
} while (true);
|
|
1385
|
+
const unavailable = !projection.runtimeReady || !projection.channelConnected;
|
|
1386
|
+
const contaminated = projection.turns > 0 || projection.pendingCount > 0;
|
|
1387
|
+
return { resetCommitted: true, generationChanged: reset.generationChanged, sessionChanged: reset.sessionChanged,
|
|
1388
|
+
...projection, readyForFreshScenario: false, inboundObserved: false,
|
|
1389
|
+
code: contaminated ? "fresh_scenario_contaminated" : unavailable ? "runtime_unavailable" : "reset_timeout",
|
|
1390
|
+
error: contaminated ? "reset committed but the fresh scenario was contaminated by a turn or Inbox arrival"
|
|
1391
|
+
: unavailable ? "reset committed but Runtime/channel readiness is unavailable"
|
|
1392
|
+
: "reset committed but readiness did not converge before timeout" };
|
|
1393
|
+
},
|
|
1394
|
+
async start() {
|
|
1395
|
+
try {
|
|
1396
|
+
node_fs_1.default.mkdirSync(larkinHome, { recursive: true });
|
|
1397
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(larkinHome, "daemon-status.json"), JSON.stringify({
|
|
1398
|
+
...(0, process_inspect_cjs_1.currentProcessMetadata)((0, internal_command_js_1.processCommandToken)("daemon", "app/runtime-process.mjs")), pid: process.pid, startedAt: daemonStartedAt,
|
|
1399
|
+
agents: agents.map((agent) => agent.agentId),
|
|
1400
|
+
}, null, 2), { mode: 0o600 });
|
|
1401
|
+
}
|
|
1402
|
+
catch (error) {
|
|
1403
|
+
log(`daemon-status 写失败: ${errorMessage(error)}`);
|
|
1404
|
+
}
|
|
1405
|
+
runtimeHost.subscribe(onRuntimeEvent);
|
|
1406
|
+
process.once("SIGINT", () => requestOrderedShutdown("SIGINT", 130));
|
|
1407
|
+
process.once("SIGTERM", () => requestOrderedShutdown("SIGTERM", 143));
|
|
1408
|
+
try {
|
|
1409
|
+
await runtimeHost.start(agents.map((agent) => ({
|
|
1410
|
+
...agent,
|
|
1411
|
+
sessionId: agentStates.get(agent.agentId)?.state.sessions[agent.runtime] || null,
|
|
1412
|
+
})));
|
|
1413
|
+
reminder.startSync();
|
|
1414
|
+
interaction.startSync();
|
|
1415
|
+
eventSourceStartTimer = setTimeout(() => { eventSourceStartTimer = null; startEventSource(); }, eventSourceStartDelayMs);
|
|
1416
|
+
}
|
|
1417
|
+
catch (error) {
|
|
1418
|
+
log(`Runtime Host 启动失败: ${errorMessage(error)}`);
|
|
1419
|
+
await requestOrderedShutdown("Runtime Host 启动失败", 1);
|
|
1420
|
+
throw error;
|
|
1421
|
+
}
|
|
1422
|
+
},
|
|
1423
|
+
shutdown,
|
|
1424
|
+
};
|
|
1425
|
+
}
|