larkin 0.5.18 → 0.5.20
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 +6 -0
- package/README.md +2 -9
- package/dist/agent/agent-state-store.cjs +11 -6
- package/dist/agent/agent-state-store.mjs +12 -7
- package/dist/agent/config-cli-contract.cjs +1 -3
- package/dist/agent/config-cli-contract.mjs +1 -3
- package/dist/agent/transport-business-context.cjs +3 -0
- package/dist/agent/transport-business-context.mjs +3 -0
- package/dist/app/agent-cli.mjs +1 -18
- package/dist/app/agent-config.mjs +4 -33
- package/dist/app/cli.mjs +0 -1
- package/dist/app/dashboard.mjs +2 -3
- package/dist/app/lark-cli.mjs +4 -0
- package/dist/app/run.mjs +5 -1
- package/dist/dashboard/dashboard-config-controller.mjs +48 -28
- package/dist/dashboard/dashboard-lifecycle.cjs +29 -0
- package/dist/dashboard/dashboard-lifecycle.mjs +28 -0
- package/dist/dashboard/web/assets/dashboard.js +15 -15
- package/dist/feishu/host-processing-eye.cjs +54 -1
- package/dist/feishu/host-processing-eye.mjs +48 -0
- package/dist/feishu/host-shell.cjs +23 -14
- package/dist/feishu/host-shell.mjs +25 -16
- package/dist/feishu/message-policy.cjs +1 -0
- package/dist/feishu/message-policy.mjs +1 -0
- package/dist/platform/config.cjs +13 -84
- package/dist/platform/config.mjs +13 -82
- package/dist/runtime/runtime-host.cjs +3 -0
- package/dist/runtime/runtime-host.mjs +3 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.19
|
|
4
|
+
|
|
5
|
+
The Feishu processing-eye OnIt reaction is no longer a config switch. After a successful deliver, Larkin reacts OnIt only for private chats (DM / p2p), when the inbound event already marked the bot as mentioned, or when the user is talking inside a topic the bot started (a Feishu topic whose root is a bot-owned message). Ordinary group messages without @ and outside a bot-started topic never get the reaction, including free-mention chats that still wake the Agent.
|
|
6
|
+
|
|
7
|
+
The 0.5.18 `processingEye.enabled` key, `larkin config processing-eye`, and Dashboard 处理中表情 toggle are removed. Leftover keys in existing configs are ignored on load and dropped on the next config write. There is no on/off flag and no migration prompt.
|
|
8
|
+
|
|
3
9
|
## 0.5.18
|
|
4
10
|
|
|
5
11
|
The Feishu processing-eye OnIt reaction is now off by default. New installs and existing configs that omit `processingEye` no longer auto-react on inbound messages after upgrade; there is no migration prompt. Opt back in with `larkin config processing-eye global on`, a per-Agent `on`/`off`/`inherit` override, or the Dashboard 处理中表情 toggle. Explicit `enabled: false` and a missing key both skip the reactions API.
|
package/README.md
CHANGED
|
@@ -114,16 +114,9 @@ larkin config inbox-audit global off
|
|
|
114
114
|
|
|
115
115
|
When enabled, audit only revisits originally wake-eligible human group/topic work and does not wake a model for an empty work list. Reading an audit list does not complete it: the managed Agent follows the returned inspection instructions and explicitly confirms the receipt after checking. A failed check remains retryable; newer messages cannot be completed by an older receipt. Old audit-index records without proven eligibility or observation identity are ignored; ordinary Inbox messages and conversation history are preserved.
|
|
116
116
|
|
|
117
|
-
###
|
|
117
|
+
### Processing eye (OnIt)
|
|
118
118
|
|
|
119
|
-
The Feishu "processing eye"
|
|
120
|
-
|
|
121
|
-
```bash
|
|
122
|
-
larkin config processing-eye global on
|
|
123
|
-
larkin config processing-eye agent off --agent <App-ID>
|
|
124
|
-
larkin config processing-eye agent inherit --agent <App-ID>
|
|
125
|
-
larkin config processing-eye global off
|
|
126
|
-
```
|
|
119
|
+
The Feishu "processing eye" is an OnIt reaction on the inbound message while the Agent works. It is automatic and not configurable: Larkin adds it after a successful deliver only for private chats (DM / p2p), when the inbound event marked the bot as mentioned, or when the user is talking inside a topic the bot started. Ordinary group messages without @ and outside a bot-started topic never get the reaction. The 0.5.18 `processingEye` config key, CLI, and Dashboard toggle are removed; leftover keys are ignored.
|
|
127
120
|
|
|
128
121
|
<details>
|
|
129
122
|
<summary>Windows support and optional autostart</summary>
|
|
@@ -232,17 +232,15 @@ function normalizeInboxState(value) {
|
|
|
232
232
|
}
|
|
233
233
|
function newInboxLockOwner(inspect) {
|
|
234
234
|
const inspected = inspect(process.pid);
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
return { version: 1, pid: process.pid, processStartToken: inspected.startToken, nonce: crypto.randomUUID() };
|
|
235
|
+
const identity = inspected.ok && inspected.startToken ? { processStartToken: inspected.startToken } : {};
|
|
236
|
+
return { version: 1, pid: process.pid, ...identity, nonce: crypto.randomUUID() };
|
|
239
237
|
}
|
|
240
238
|
function validInboxLockOwner(value) {
|
|
241
239
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
242
240
|
return false;
|
|
243
241
|
const owner = value;
|
|
244
242
|
return owner.version === 1 && Number.isSafeInteger(owner.pid) && Number(owner.pid) > 0
|
|
245
|
-
&& typeof owner.processStartToken === "string" && !!owner.processStartToken
|
|
243
|
+
&& (owner.processStartToken === undefined || (typeof owner.processStartToken === "string" && !!owner.processStartToken))
|
|
246
244
|
&& typeof owner.nonce === "string" && /^[0-9a-f-]{36}$/i.test(owner.nonce);
|
|
247
245
|
}
|
|
248
246
|
class ContextOverflowRecoveryError extends Error {
|
|
@@ -308,6 +306,12 @@ class AgentStateStore {
|
|
|
308
306
|
const age = Date.now() - stat.mtimeMs;
|
|
309
307
|
return age >= INBOX_MALFORMED_LOCK_GRACE_MS ? "reclaimable" : "unknown";
|
|
310
308
|
}
|
|
309
|
+
if (!(0, process_state_js_1.pidAlive)(owner.pid))
|
|
310
|
+
return "reclaimable";
|
|
311
|
+
// A live owner is reclaimed only when its start token proves the pid was reused. Without a
|
|
312
|
+
// token to compare, the lock may still be held, and its owner may still write back.
|
|
313
|
+
if (!owner.processStartToken)
|
|
314
|
+
return "unknown";
|
|
311
315
|
const inspected = this.inspect(owner.pid);
|
|
312
316
|
if (!inspected.ok)
|
|
313
317
|
return inspected.dead ? "reclaimable" : "unknown";
|
|
@@ -333,7 +337,8 @@ class AgentStateStore {
|
|
|
333
337
|
});
|
|
334
338
|
}
|
|
335
339
|
catch (error) {
|
|
336
|
-
|
|
340
|
+
// A sandboxed process cannot record its own identity for the reclaim lock; it waits for another process to reclaim.
|
|
341
|
+
if (/lock 已被|无法取得 lock|正在创建|暂不能接管|无法读取当前进程身份/.test(error instanceof Error ? error.message : String(error)))
|
|
337
342
|
return false;
|
|
338
343
|
throw error;
|
|
339
344
|
}
|
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as crypto from "node:crypto";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { TargetRootLayout } from "../platform/root-layout.mjs";
|
|
5
|
-
import { acquireProcessLock, inspectProcess } from "../platform/process-state.mjs";
|
|
5
|
+
import { acquireProcessLock, inspectProcess, pidAlive } from "../platform/process-state.mjs";
|
|
6
6
|
import { isWindows } from "../platform/secure-metadata.mjs";
|
|
7
7
|
import { isCanonicalInboxTarget, isUserDeliveryTarget, RUNTIME_REMINDER_TARGET, targetKeyOfInboxEnvelope } from "./inbox-projection.mjs";
|
|
8
8
|
import { buildStrictProviderErrorInput, classifyStrictProviderError } from "../runtime/provider-error-classifier.mjs";
|
|
@@ -195,17 +195,15 @@ function normalizeInboxState(value) {
|
|
|
195
195
|
}
|
|
196
196
|
function newInboxLockOwner(inspect) {
|
|
197
197
|
const inspected = inspect(process.pid);
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
return { version: 1, pid: process.pid, processStartToken: inspected.startToken, nonce: crypto.randomUUID() };
|
|
198
|
+
const identity = inspected.ok && inspected.startToken ? { processStartToken: inspected.startToken } : {};
|
|
199
|
+
return { version: 1, pid: process.pid, ...identity, nonce: crypto.randomUUID() };
|
|
202
200
|
}
|
|
203
201
|
function validInboxLockOwner(value) {
|
|
204
202
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
205
203
|
return false;
|
|
206
204
|
const owner = value;
|
|
207
205
|
return owner.version === 1 && Number.isSafeInteger(owner.pid) && Number(owner.pid) > 0
|
|
208
|
-
&& typeof owner.processStartToken === "string" && !!owner.processStartToken
|
|
206
|
+
&& (owner.processStartToken === undefined || (typeof owner.processStartToken === "string" && !!owner.processStartToken))
|
|
209
207
|
&& typeof owner.nonce === "string" && /^[0-9a-f-]{36}$/i.test(owner.nonce);
|
|
210
208
|
}
|
|
211
209
|
export class ContextOverflowRecoveryError extends Error {
|
|
@@ -270,6 +268,12 @@ export class AgentStateStore {
|
|
|
270
268
|
const age = Date.now() - stat.mtimeMs;
|
|
271
269
|
return age >= INBOX_MALFORMED_LOCK_GRACE_MS ? "reclaimable" : "unknown";
|
|
272
270
|
}
|
|
271
|
+
if (!pidAlive(owner.pid))
|
|
272
|
+
return "reclaimable";
|
|
273
|
+
// A live owner is reclaimed only when its start token proves the pid was reused. Without a
|
|
274
|
+
// token to compare, the lock may still be held, and its owner may still write back.
|
|
275
|
+
if (!owner.processStartToken)
|
|
276
|
+
return "unknown";
|
|
273
277
|
const inspected = this.inspect(owner.pid);
|
|
274
278
|
if (!inspected.ok)
|
|
275
279
|
return inspected.dead ? "reclaimable" : "unknown";
|
|
@@ -295,7 +299,8 @@ export class AgentStateStore {
|
|
|
295
299
|
});
|
|
296
300
|
}
|
|
297
301
|
catch (error) {
|
|
298
|
-
|
|
302
|
+
// A sandboxed process cannot record its own identity for the reclaim lock; it waits for another process to reclaim.
|
|
303
|
+
if (/lock 已被|无法取得 lock|正在创建|暂不能接管|无法读取当前进程身份/.test(error instanceof Error ? error.message : String(error)))
|
|
299
304
|
return false;
|
|
300
305
|
throw error;
|
|
301
306
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CONFIG_CLI_VALUES = exports.CONFIG_CLI_USAGE = exports.CONFIG_CLI_OPERATIONS = void 0;
|
|
4
|
-
exports.CONFIG_CLI_OPERATIONS = Object.freeze(["show", "runtime", "model", "effort", "mention", "inbox-audit", "
|
|
4
|
+
exports.CONFIG_CLI_OPERATIONS = Object.freeze(["show", "runtime", "model", "effort", "mention", "inbox-audit", "apply"]);
|
|
5
5
|
exports.CONFIG_CLI_USAGE = Object.freeze([
|
|
6
6
|
"larkin config show [--agent <App ID>] [--chat <oc_id>] [--json]",
|
|
7
7
|
"larkin config runtime <runtime> [--model <model>] [--agent <App ID>]",
|
|
@@ -12,8 +12,6 @@ exports.CONFIG_CLI_USAGE = Object.freeze([
|
|
|
12
12
|
"larkin config mention chat <oc_id> <inherit|require|free> [--agent <App ID>]",
|
|
13
13
|
"larkin config inbox-audit global <on|off> [--interval <15m|1h>]",
|
|
14
14
|
"larkin config inbox-audit agent <inherit|on|off> [--agent <App ID>] [--interval <15m|inherit>]",
|
|
15
|
-
"larkin config processing-eye global <on|off>",
|
|
16
|
-
"larkin config processing-eye agent <inherit|on|off> [--agent <App ID>]",
|
|
17
15
|
"larkin config apply [--agent <App ID>]",
|
|
18
16
|
]);
|
|
19
17
|
exports.CONFIG_CLI_VALUES = Object.freeze({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const CONFIG_CLI_OPERATIONS = Object.freeze(["show", "runtime", "model", "effort", "mention", "inbox-audit", "
|
|
1
|
+
export const CONFIG_CLI_OPERATIONS = Object.freeze(["show", "runtime", "model", "effort", "mention", "inbox-audit", "apply"]);
|
|
2
2
|
export const CONFIG_CLI_USAGE = Object.freeze([
|
|
3
3
|
"larkin config show [--agent <App ID>] [--chat <oc_id>] [--json]",
|
|
4
4
|
"larkin config runtime <runtime> [--model <model>] [--agent <App ID>]",
|
|
@@ -9,8 +9,6 @@ export const CONFIG_CLI_USAGE = Object.freeze([
|
|
|
9
9
|
"larkin config mention chat <oc_id> <inherit|require|free> [--agent <App ID>]",
|
|
10
10
|
"larkin config inbox-audit global <on|off> [--interval <15m|1h>]",
|
|
11
11
|
"larkin config inbox-audit agent <inherit|on|off> [--agent <App ID>] [--interval <15m|inherit>]",
|
|
12
|
-
"larkin config processing-eye global <on|off>",
|
|
13
|
-
"larkin config processing-eye agent <inherit|on|off> [--agent <App ID>]",
|
|
14
12
|
"larkin config apply [--agent <App ID>]",
|
|
15
13
|
]);
|
|
16
14
|
export const CONFIG_CLI_VALUES = Object.freeze({
|
|
@@ -49,6 +49,7 @@ const reminder_routes_js_1 = require("./reminder-routes.cjs");
|
|
|
49
49
|
const reminder_delivery_audit_js_1 = require("./reminder-delivery-audit.cjs");
|
|
50
50
|
const transport_shell_js_1 = require("./transport-shell.cjs");
|
|
51
51
|
const agent_lark_cli_workspace_js_1 = require("../app/agent-lark-cli-workspace.cjs");
|
|
52
|
+
const host_processing_eye_js_1 = require("../feishu/host-processing-eye.cjs");
|
|
52
53
|
function formatFeishuError(error, grantLink = null) {
|
|
53
54
|
if (!error)
|
|
54
55
|
return null;
|
|
@@ -139,6 +140,8 @@ function createTransportBusinessContext(env = process.env) {
|
|
|
139
140
|
const appendConversation = (item) => {
|
|
140
141
|
try {
|
|
141
142
|
fs.appendFileSync(paths.conversation, `${JSON.stringify({ ...item, text: (0, transport_shell_js_1.safeConversationExcerpt)(item.text) })}\n`);
|
|
143
|
+
if (item.direction === "out")
|
|
144
|
+
(0, host_processing_eye_js_1.persistBotTopicRoot)(stateStore, typeof item.messageId === "string" ? item.messageId : null);
|
|
142
145
|
}
|
|
143
146
|
catch (error) {
|
|
144
147
|
log(`conversation 写失败: ${error.message}`);
|
|
@@ -12,6 +12,7 @@ import { createReminderRoutes } from "./reminder-routes.mjs";
|
|
|
12
12
|
import { auditReminderDelivery } from "./reminder-delivery-audit.mjs";
|
|
13
13
|
import { isImageMime, looksLikeMarkdown, resolveMappedChatId, safeConversationExcerpt } from "./transport-shell.mjs";
|
|
14
14
|
import { managedOfficialLarkCli } from "../app/agent-lark-cli-workspace.mjs";
|
|
15
|
+
import { persistBotTopicRoot } from "../feishu/host-processing-eye.mjs";
|
|
15
16
|
export function formatFeishuError(error, grantLink = null) {
|
|
16
17
|
if (!error)
|
|
17
18
|
return null;
|
|
@@ -102,6 +103,8 @@ export function createTransportBusinessContext(env = process.env) {
|
|
|
102
103
|
const appendConversation = (item) => {
|
|
103
104
|
try {
|
|
104
105
|
fs.appendFileSync(paths.conversation, `${JSON.stringify({ ...item, text: safeConversationExcerpt(item.text) })}\n`);
|
|
106
|
+
if (item.direction === "out")
|
|
107
|
+
persistBotTopicRoot(stateStore, typeof item.messageId === "string" ? item.messageId : null);
|
|
105
108
|
}
|
|
106
109
|
catch (error) {
|
|
107
110
|
log(`conversation 写失败: ${error.message}`);
|
package/dist/app/agent-cli.mjs
CHANGED
|
@@ -256,23 +256,6 @@ function agentConfigRequest(argv, env, config, agent, dependencies) {
|
|
|
256
256
|
agentId: targetId,
|
|
257
257
|
});
|
|
258
258
|
}
|
|
259
|
-
else if (operation === "processing-eye") {
|
|
260
|
-
const [scope, first] = options.positionals;
|
|
261
|
-
if (scope === "global")
|
|
262
|
-
assertOnlyFlags([]);
|
|
263
|
-
else if (scope === "agent")
|
|
264
|
-
assertOnlyFlags(["--agent"]);
|
|
265
|
-
else
|
|
266
|
-
assertOnlyFlags([]);
|
|
267
|
-
if (options.positionals.length !== 2) {
|
|
268
|
-
throw new Error("用法: larkin config processing-eye global <on|off> | processing-eye agent <inherit|on|off> [--agent <App ID>]");
|
|
269
|
-
}
|
|
270
|
-
mutation = larkinConfig.processingEyeMutationFromCli({
|
|
271
|
-
scope: scope || "",
|
|
272
|
-
enabled: first,
|
|
273
|
-
agentId: targetId,
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
259
|
else if (operation === "apply") {
|
|
277
260
|
assertOnlyFlags(["--agent"]);
|
|
278
261
|
if (options.positionals.length)
|
|
@@ -286,7 +269,7 @@ function agentConfigRequest(argv, env, config, agent, dependencies) {
|
|
|
286
269
|
}).catch((error) => { throw new Error(`配置已保存但未应用:${error instanceof Error ? error.message : String(error)}`); });
|
|
287
270
|
}
|
|
288
271
|
else
|
|
289
|
-
throw new Error("config 只支持 show/runtime/model/effort/mention/inbox-audit/
|
|
272
|
+
throw new Error("config 只支持 show/runtime/model/effort/mention/inbox-audit/apply;运行 larkin config --help");
|
|
290
273
|
const result = larkinConfig.mutateConfig(env, mutation, authority);
|
|
291
274
|
return { ok: true, revision: result.revision, persisted: true, applyState: result.applyState, changedScope: result.changedScope };
|
|
292
275
|
}
|
|
@@ -52,7 +52,7 @@ const die = (message) => {
|
|
|
52
52
|
process.exit(1);
|
|
53
53
|
};
|
|
54
54
|
if (!kind || !["agents", "model", "runtime", "effort", "chats", "config"].includes(kind)) {
|
|
55
|
-
die("用法: larkin agents | larkin config <show|mention|inbox-audit|
|
|
55
|
+
die("用法: larkin agents | larkin config <show|mention|inbox-audit|apply> | larkin model [<id>] | larkin runtime [<id>] [--model <id>] | larkin effort [<level>|clear] | larkin chats [free|strict <oc_id>]");
|
|
56
56
|
}
|
|
57
57
|
const allowedValueFlags = {
|
|
58
58
|
agents: new Set(), model: new Set(["--agent"]), runtime: new Set(["--agent", "--model"]),
|
|
@@ -162,23 +162,13 @@ else if (kind === "config") {
|
|
|
162
162
|
if (positionals.length !== 3)
|
|
163
163
|
die("用法: larkin config inbox-audit agent <inherit|on|off> [--agent <App ID>] [--interval <15m|inherit>]");
|
|
164
164
|
}
|
|
165
|
-
else if (operation === "processing-eye" && scope === "global") {
|
|
166
|
-
assertOnlyFlags([]);
|
|
167
|
-
if (positionals.length !== 3)
|
|
168
|
-
die("用法: larkin config processing-eye global <on|off>");
|
|
169
|
-
}
|
|
170
|
-
else if (operation === "processing-eye" && scope === "agent") {
|
|
171
|
-
assertOnlyFlags(["--agent"]);
|
|
172
|
-
if (positionals.length !== 3)
|
|
173
|
-
die("用法: larkin config processing-eye agent <inherit|on|off> [--agent <App ID>]");
|
|
174
|
-
}
|
|
175
165
|
else if (operation === "apply") {
|
|
176
166
|
assertOnlyFlags(["--agent"]);
|
|
177
167
|
if (positionals.length !== 1)
|
|
178
168
|
die("用法: larkin config apply [--agent <App ID>]");
|
|
179
169
|
}
|
|
180
170
|
else {
|
|
181
|
-
die("config 只支持 show/mention/inbox-audit/
|
|
171
|
+
die("config 只支持 show/mention/inbox-audit/apply;运行 larkin config --help");
|
|
182
172
|
}
|
|
183
173
|
}
|
|
184
174
|
const loaded = (() => {
|
|
@@ -362,20 +352,15 @@ if (kind === "config") {
|
|
|
362
352
|
say(`全局群消息策略=${String(view.mentionPolicy)}(free 会唤醒所有继承它的 Agent)`);
|
|
363
353
|
const inboxAudit = view.inboxAudit;
|
|
364
354
|
say(`全局 Inbox Audit=${inboxAudit?.enabled ? "on" : "off"} interval=${larkinConfig.formatInboxAuditInterval(Number(inboxAudit?.intervalMs || larkinConfig.DEFAULT_INBOX_AUDIT_INTERVAL_MS))}(默认关闭;未 @ 的 require 群消息不会进入审计)`);
|
|
365
|
-
const processingEye = view.processingEye;
|
|
366
|
-
say(`全局处理中表情=${processingEye?.enabled ? "on" : "off"}(默认关闭;缺 key 视为关闭,升级后不再自动点 OnIt)`);
|
|
367
355
|
for (const item of view.agents) {
|
|
368
356
|
const mention = item.mention;
|
|
369
357
|
const agentAudit = item.inboxAudit;
|
|
370
|
-
const agentEye = item.processingEye;
|
|
371
358
|
const apply = item.apply;
|
|
372
|
-
say(`agent=${item.agentId} runtime=${item.runtime} model=${item.model} effort=${item.effort || "default"} mention=${mention.override} inbox-audit=${agentAudit?.override?.enabled || "inherit"}
|
|
359
|
+
say(`agent=${item.agentId} runtime=${item.runtime} model=${item.model} effort=${item.effort || "default"} mention=${mention.override} inbox-audit=${agentAudit?.override?.enabled || "inherit"} apply=${apply.applyState || "unknown"}`);
|
|
373
360
|
if (mention.chat)
|
|
374
361
|
say(` chat=${mention.chat.chatId} override=${mention.chat.override} effective=${mention.chat.effective} source=${mention.chat.source}`);
|
|
375
362
|
if (agentAudit?.effective)
|
|
376
363
|
say(` inbox-audit effective=${agentAudit.effective.enabled ? "on" : "off"} interval=${larkinConfig.formatInboxAuditInterval(Number(agentAudit.effective.intervalMs || larkinConfig.DEFAULT_INBOX_AUDIT_INTERVAL_MS))}`);
|
|
377
|
-
if (agentEye?.effective)
|
|
378
|
-
say(` processing-eye effective=${agentEye.effective.enabled ? "on" : "off"}`);
|
|
379
364
|
}
|
|
380
365
|
}
|
|
381
366
|
process.exit(0);
|
|
@@ -420,20 +405,6 @@ if (kind === "config") {
|
|
|
420
405
|
}
|
|
421
406
|
process.exit(0);
|
|
422
407
|
}
|
|
423
|
-
if (operation === "processing-eye") {
|
|
424
|
-
try {
|
|
425
|
-
const result = larkinConfig.mutateConfig(process.env, larkinConfig.processingEyeMutationFromCli({
|
|
426
|
-
scope: scope || "",
|
|
427
|
-
enabled: first,
|
|
428
|
-
agentId: scope === "agent" ? requireTarget() : (selected || keys[0] || ""),
|
|
429
|
-
}), authority);
|
|
430
|
-
say(JSON.stringify({ ok: true, revision: result.revision, persisted: result.persisted, applyState: result.applyState, changedScope: result.changedScope }, null, 2));
|
|
431
|
-
}
|
|
432
|
-
catch (error) {
|
|
433
|
-
die(error instanceof Error ? error.message : String(error));
|
|
434
|
-
}
|
|
435
|
-
process.exit(0);
|
|
436
|
-
}
|
|
437
408
|
if (operation === "apply") {
|
|
438
409
|
const agentId = requireTarget();
|
|
439
410
|
const expectedSignature = larkinConfig.runtimeConfigSignature(config, agentId);
|
|
@@ -453,7 +424,7 @@ if (kind === "config") {
|
|
|
453
424
|
}
|
|
454
425
|
process.exit(0);
|
|
455
426
|
}
|
|
456
|
-
die("config 只支持 show/mention/inbox-audit/
|
|
427
|
+
die("config 只支持 show/mention/inbox-audit/apply");
|
|
457
428
|
}
|
|
458
429
|
let key = flagAgent;
|
|
459
430
|
if (!key && runtimeAgentId)
|
package/dist/app/cli.mjs
CHANGED
|
@@ -87,7 +87,6 @@ Examples:
|
|
|
87
87
|
larkin config runtime pi --agent cli_x --model default
|
|
88
88
|
larkin config mention chat oc_x free --agent cli_x
|
|
89
89
|
larkin config inbox-audit global on --interval 15m
|
|
90
|
-
larkin config processing-eye global on
|
|
91
90
|
|
|
92
91
|
Credentials, internal paths, serverId, activeAgent, and raw config are never exposed here.`,
|
|
93
92
|
session: `Usage: larkin session reset --agent <App ID> --json [--wait-ready <seconds>]
|
package/dist/app/dashboard.mjs
CHANGED
|
@@ -13,11 +13,10 @@ import "../platform/check-bun-version.cjs";
|
|
|
13
13
|
import fs from "node:fs";
|
|
14
14
|
import http from "node:http";
|
|
15
15
|
import path from "node:path";
|
|
16
|
-
import crypto from "node:crypto";
|
|
17
16
|
import { spawnSync } from "node:child_process";
|
|
18
17
|
import { fileURLToPath } from "node:url";
|
|
19
18
|
import { currentProcessMetadata, readJson } from "../platform/process-state.mjs";
|
|
20
|
-
import { prepareDashboardLaunch } from "../dashboard/dashboard-lifecycle.mjs";
|
|
19
|
+
import { loadDashboardCapability, prepareDashboardLaunch } from "../dashboard/dashboard-lifecycle.mjs";
|
|
21
20
|
import { DASHBOARD_BUILD_FINGERPRINT, DASHBOARD_BUILD_VERSION, PACKAGE_VERSION, collectStatus, collectWorkspaceEntry, loadDashboardConfig, resolveDashboardAvatarSource } from "../dashboard/dashboard-view-model.mjs";
|
|
22
21
|
import { renderDashboardHtml } from "../dashboard/dashboard-template.mjs";
|
|
23
22
|
import { createDashboardConfigController, createPiModelDirectoryResolver } from "../dashboard/dashboard-config-controller.mjs";
|
|
@@ -64,7 +63,7 @@ if (preparedDashboard.action === "reuse") {
|
|
|
64
63
|
process.exit(0);
|
|
65
64
|
}
|
|
66
65
|
const dashboardLaunchLock = { release: preparedDashboard.release };
|
|
67
|
-
const csrfCapability =
|
|
66
|
+
const csrfCapability = loadDashboardCapability(path.join(startupConfig.larkinHome, "dashboard-capability"));
|
|
68
67
|
const HTML = renderDashboardHtml(PACKAGE_VERSION, DASHBOARD_BUILD_VERSION, BUILD_FINGERPRINT, csrfCapability);
|
|
69
68
|
const piModelResolver = createPiModelDirectoryResolver();
|
|
70
69
|
const configController = createDashboardConfigController({ csrfCapability, piModelDirectoryResolver: piModelResolver });
|
package/dist/app/lark-cli.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import * as larkinConfig from "../platform/config.mjs";
|
|
|
11
11
|
import { resolveOfficialLarkCli } from "./official-lark-cli.mjs";
|
|
12
12
|
import { assertAgentWorkspaceBound, managedLarkCliEnv } from "./agent-lark-cli-workspace.mjs";
|
|
13
13
|
import { parseDocumentCommentTarget } from "../feishu/document-comment.mjs";
|
|
14
|
+
import { persistBotTopicRoot } from "../feishu/host-processing-eye.mjs";
|
|
14
15
|
import { SpanKind } from "@opentelemetry/api";
|
|
15
16
|
import { loadTelemetryConfig } from "../platform/telemetry-config.mjs";
|
|
16
17
|
import { telemetrySingleton } from "../platform/telemetry-tracing.mjs";
|
|
@@ -1294,6 +1295,9 @@ export function runLarkCli(argv, env = process.env, dependencies = {}) {
|
|
|
1294
1295
|
const deliveryAnchor = currentReminder?.deliveryAnchor;
|
|
1295
1296
|
const memo = !write.error && write.status === 0 && writeMessage
|
|
1296
1297
|
? recordImWriteMemo(store, intentKey, writeMessage.message_id, deliveryAnchor) : { duplicate: false };
|
|
1298
|
+
if (!write.error && write.status === 0 && writeMessage?.message_id) {
|
|
1299
|
+
persistBotTopicRoot(store, writeMessage.message_id);
|
|
1300
|
+
}
|
|
1297
1301
|
const duplicateOfEarlierReminder = memo.duplicate && Boolean(currentReminder)
|
|
1298
1302
|
&& memo.priorSourceMessageId !== currentReminder?.deliveryAnchor;
|
|
1299
1303
|
const committedWrite = !write.error && write.status === 0 && !duplicateOfEarlierReminder;
|
package/dist/app/run.mjs
CHANGED
|
@@ -385,10 +385,12 @@ export async function main() {
|
|
|
385
385
|
launchLock.release();
|
|
386
386
|
console.error(`[start] unified supervisor pid=${process.pid} daemon=${daemon.pid} agents=${names.join(",")}`);
|
|
387
387
|
console.error("[start] dashboard 已随服务启动;不会自动打开浏览器");
|
|
388
|
-
const requestStop = (signal) => {
|
|
388
|
+
const requestStop = (signal, received = signal) => {
|
|
389
389
|
if (stopping)
|
|
390
390
|
return;
|
|
391
391
|
stopping = true;
|
|
392
|
+
// Name the stop signal so a service that vanished can be told apart from a crash (a crash logs its own line).
|
|
393
|
+
console.error(`[start] 收到 ${received},停止服务 (pid=${process.pid} ppid=${process.ppid})`);
|
|
392
394
|
if (daemonRestartTimer) {
|
|
393
395
|
clearTimeout(daemonRestartTimer);
|
|
394
396
|
daemonRestartTimer = null;
|
|
@@ -405,6 +407,8 @@ export async function main() {
|
|
|
405
407
|
};
|
|
406
408
|
process.once("SIGINT", () => requestStop("SIGINT"));
|
|
407
409
|
process.once("SIGTERM", () => requestStop("SIGTERM"));
|
|
410
|
+
// A closed terminal would otherwise end the supervisor without a trace; stop the same way as SIGTERM.
|
|
411
|
+
process.once("SIGHUP", () => requestStop("SIGTERM", "SIGHUP"));
|
|
408
412
|
const daemonResult = await daemonFinal;
|
|
409
413
|
try {
|
|
410
414
|
cleanupStaleAgentControlSocket(configDir, controlToken);
|
|
@@ -310,9 +310,12 @@ export function createClaudeModelDirectoryResolver(options = {}) {
|
|
|
310
310
|
},
|
|
311
311
|
};
|
|
312
312
|
}
|
|
313
|
-
async function sanitizedView(env, chatDirectoryResolver, onlyAgentId, chatId) {
|
|
313
|
+
async function sanitizedView(env, chatDirectoryResolver, onlyAgentId, chatId, withChats = true) {
|
|
314
314
|
const { config } = loadConfig(env);
|
|
315
315
|
const view = safeConfigView(config, onlyAgentId, chatId, configApplyState(env, config));
|
|
316
|
+
// Group names come from Feishu, one lookup per Agent; callers that only need the global values skip them.
|
|
317
|
+
if (!withChats)
|
|
318
|
+
return view;
|
|
316
319
|
for (const item of view.agents) {
|
|
317
320
|
const agentId = String(item.agentId);
|
|
318
321
|
const agent = config.agents[agentId];
|
|
@@ -367,6 +370,37 @@ function assertWriteRequest(req, csrfCapability) {
|
|
|
367
370
|
if (!String(req.headers["content-type"] || "").toLowerCase().startsWith("application/json"))
|
|
368
371
|
throw new Error("application/json required");
|
|
369
372
|
}
|
|
373
|
+
/**
|
|
374
|
+
* Map a failed dashboard request to a status and a message the page shows as is.
|
|
375
|
+
* The raw reason goes to the local log. A wrong capability means the page was opened before this dashboard
|
|
376
|
+
* (re)started; an unknown operation or field means the page is from another build; both are fixed by a reload.
|
|
377
|
+
*/
|
|
378
|
+
export function dashboardError(error, fallback) {
|
|
379
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
380
|
+
console.warn(`[dashboard] ${fallback}: ${message}`);
|
|
381
|
+
const reply = (status, text) => ({ status, body: { error: text } });
|
|
382
|
+
if (/^(?:csrf|capability) rejected$/.test(message))
|
|
383
|
+
return reply(403, "这个页面已过期,刷新页面后再操作。");
|
|
384
|
+
if (/^(?:invalid host|same-origin required)$/.test(message))
|
|
385
|
+
return reply(403, "请在本机用 http://localhost 打开 dashboard。");
|
|
386
|
+
if (message === "request too large")
|
|
387
|
+
return reply(413, "内容太大,无法保存。");
|
|
388
|
+
if (message === "unknown agent")
|
|
389
|
+
return reply(404, "这个 Agent 已不存在,刷新页面看看。");
|
|
390
|
+
if (/not in current (?:runtime|model) directory$/.test(message))
|
|
391
|
+
return reply(400, "所选的模型或推理强度已不在可用列表里,重新选择后再保存。");
|
|
392
|
+
if (/^(?:unsupported operation|invalid |JSON object required|application\/json required)/.test(message) || error instanceof SyntaxError) {
|
|
393
|
+
return reply(400, "页面和正在运行的 larkin 版本不一致。刷新页面后再操作。");
|
|
394
|
+
}
|
|
395
|
+
// Messages written for people (Chinese) are shown as is; anything else keeps the fallback plus its reason.
|
|
396
|
+
if (/[\u4e00-\u9fff]/.test(message))
|
|
397
|
+
return reply(400, message);
|
|
398
|
+
return reply(500, `${fallback}:${message.slice(0, 200)}`);
|
|
399
|
+
}
|
|
400
|
+
function sendError(res, error, fallback, extra = {}) {
|
|
401
|
+
const { status, body } = dashboardError(error, fallback);
|
|
402
|
+
json(res, status, { ...body, ...extra });
|
|
403
|
+
}
|
|
370
404
|
function assertPrivateReadRequest(req, csrfCapability) {
|
|
371
405
|
const host = String(req.headers.host || "");
|
|
372
406
|
if (!/^(?:localhost|127\.0\.0\.1):\d+$/.test(host))
|
|
@@ -402,17 +436,11 @@ function dashboardMutation(value) {
|
|
|
402
436
|
"set-chat-mention": ["operation", "agentId", "chatId", "value"], "set-agent-runtime": ["operation", "agentId", "runtime", "model"],
|
|
403
437
|
"set-agent-model": ["operation", "agentId", "model"], "set-agent-effort": ["operation", "agentId", "effort"],
|
|
404
438
|
"set-global-inbox-audit": ["operation", "enabled", "intervalMs"], "set-agent-inbox-audit": ["operation", "agentId", "enabled", "intervalMs"],
|
|
405
|
-
"set-global-processing-eye": ["operation", "enabled"], "set-agent-processing-eye": ["operation", "agentId", "enabled"],
|
|
406
439
|
};
|
|
407
440
|
if (!allowed[operation] || Object.keys(value).some((key) => !allowed[operation].includes(key)))
|
|
408
441
|
throw new Error("unsupported operation");
|
|
409
442
|
if (operation === "set-global-mention")
|
|
410
443
|
return { kind: operation, value: String(value.value) };
|
|
411
|
-
if (operation === "set-global-processing-eye") {
|
|
412
|
-
if (typeof value.enabled !== "boolean")
|
|
413
|
-
throw new Error("invalid processing-eye enabled");
|
|
414
|
-
return { kind: operation, enabled: value.enabled };
|
|
415
|
-
}
|
|
416
444
|
if (operation === "set-global-inbox-audit") {
|
|
417
445
|
if (value.enabled !== undefined && typeof value.enabled !== "boolean")
|
|
418
446
|
throw new Error("invalid inbox-audit enabled");
|
|
@@ -428,11 +456,6 @@ function dashboardMutation(value) {
|
|
|
428
456
|
throw new Error("invalid inbox-audit interval");
|
|
429
457
|
return { kind: operation, agentId, ...(value.enabled !== undefined ? { enabled: value.enabled } : {}), ...(value.intervalMs !== undefined ? { intervalMs: value.intervalMs } : {}) };
|
|
430
458
|
}
|
|
431
|
-
if (operation === "set-agent-processing-eye") {
|
|
432
|
-
if (value.enabled !== "inherit" && typeof value.enabled !== "boolean")
|
|
433
|
-
throw new Error("invalid processing-eye enabled");
|
|
434
|
-
return { kind: operation, agentId, enabled: value.enabled };
|
|
435
|
-
}
|
|
436
459
|
if (operation === "set-agent-mention")
|
|
437
460
|
return { kind: operation, agentId, value: String(value.value) };
|
|
438
461
|
if (operation === "set-chat-mention")
|
|
@@ -519,13 +542,13 @@ export function createDashboardConfigController({ csrfCapability, env = process.
|
|
|
519
542
|
try {
|
|
520
543
|
assertPrivateReadRequest(req, csrfCapability);
|
|
521
544
|
json(res, 200, {
|
|
522
|
-
...await sanitizedView(env, chatDirectoryResolver, requestUrl.searchParams.get("agent") || undefined, requestUrl.searchParams.get("chat") || undefined),
|
|
545
|
+
...await sanitizedView(env, chatDirectoryResolver, requestUrl.searchParams.get("agent") || undefined, requestUrl.searchParams.get("chat") || undefined, requestUrl.searchParams.get("chats") !== "0"),
|
|
523
546
|
runtimeModels: loadRuntimeModels(),
|
|
524
547
|
runtimeOptions: [...RUNTIME_OPTIONS],
|
|
525
548
|
});
|
|
526
549
|
}
|
|
527
550
|
catch (error) {
|
|
528
|
-
|
|
551
|
+
sendError(res, error, "读取配置失败");
|
|
529
552
|
}
|
|
530
553
|
return true;
|
|
531
554
|
}
|
|
@@ -537,7 +560,7 @@ export function createDashboardConfigController({ csrfCapability, env = process.
|
|
|
537
560
|
json(res, 200, { models });
|
|
538
561
|
}
|
|
539
562
|
catch (error) {
|
|
540
|
-
|
|
563
|
+
sendError(res, error, "读取 Pi 模型列表失败");
|
|
541
564
|
}
|
|
542
565
|
return true;
|
|
543
566
|
}
|
|
@@ -553,7 +576,7 @@ export function createDashboardConfigController({ csrfCapability, env = process.
|
|
|
553
576
|
json(res, 200, { models });
|
|
554
577
|
}
|
|
555
578
|
catch (error) {
|
|
556
|
-
|
|
579
|
+
sendError(res, error, "读取 Codex 模型列表失败");
|
|
557
580
|
}
|
|
558
581
|
return true;
|
|
559
582
|
}
|
|
@@ -569,7 +592,7 @@ export function createDashboardConfigController({ csrfCapability, env = process.
|
|
|
569
592
|
json(res, 200, { models });
|
|
570
593
|
}
|
|
571
594
|
catch (error) {
|
|
572
|
-
|
|
595
|
+
sendError(res, error, "读取 Claude 模型列表失败");
|
|
573
596
|
}
|
|
574
597
|
return true;
|
|
575
598
|
}
|
|
@@ -583,15 +606,10 @@ export function createDashboardConfigController({ csrfCapability, env = process.
|
|
|
583
606
|
json(res, 200, { ok: true, revision: result.revision, persisted: true, applyState: result.applyState, changedScope: result.changedScope });
|
|
584
607
|
}
|
|
585
608
|
catch (error) {
|
|
586
|
-
if (error instanceof RuntimePrerequisiteError)
|
|
609
|
+
if (error instanceof RuntimePrerequisiteError)
|
|
587
610
|
json(res, 400, { error: error.message, readiness: error.readiness });
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
json(res, 400, { error: error.message });
|
|
591
|
-
}
|
|
592
|
-
else {
|
|
593
|
-
json(res, 400, { error: "configuration update rejected" });
|
|
594
|
-
}
|
|
611
|
+
else
|
|
612
|
+
sendError(res, error, "保存失败");
|
|
595
613
|
}
|
|
596
614
|
return true;
|
|
597
615
|
}
|
|
@@ -608,15 +626,17 @@ export function createDashboardConfigController({ csrfCapability, env = process.
|
|
|
608
626
|
const expectedSignature = runtimeConfigSignature(config, agentId);
|
|
609
627
|
const result = await requestUpsert({ larkinHome: config.larkinHome, agentId });
|
|
610
628
|
if (!result.ok) {
|
|
611
|
-
json(res, 409, { error: result.error || "
|
|
629
|
+
json(res, 409, { error: result.error || "配置已保存,但应用失败",
|
|
612
630
|
...(result.readiness ? { readiness: result.readiness } : {}) });
|
|
613
631
|
return true;
|
|
614
632
|
}
|
|
615
633
|
markConfigApplied(env, agentId, expectedSignature);
|
|
616
634
|
json(res, 200, { ok: true, agentId, applyState: "applied" });
|
|
617
635
|
}
|
|
618
|
-
catch {
|
|
619
|
-
|
|
636
|
+
catch (error) {
|
|
637
|
+
const { status, body } = dashboardError(error, "配置已保存,但应用失败");
|
|
638
|
+
// An unreachable runtime is a failed apply of a saved config, not a server error.
|
|
639
|
+
json(res, status === 500 ? 409 : status, body);
|
|
620
640
|
}
|
|
621
641
|
return true;
|
|
622
642
|
}
|
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loadDashboardCapability = loadDashboardCapability;
|
|
6
7
|
exports.dashboardBuildFingerprint = dashboardBuildFingerprint;
|
|
7
8
|
exports.dashboardReuseDecision = dashboardReuseDecision;
|
|
8
9
|
exports.reconcileDashboardRecord = reconcileDashboardRecord;
|
|
@@ -12,6 +13,7 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
12
13
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
13
14
|
const process_state_js_1 = require("../platform/process-state.cjs");
|
|
14
15
|
const internal_command_js_1 = require("../app/internal-command.cjs");
|
|
16
|
+
const secure_metadata_js_1 = require("../platform/secure-metadata.cjs");
|
|
15
17
|
const PUBLISHED_RUNTIME_DIRS = ["dist", "assets"];
|
|
16
18
|
const RUNTIME_EXTENSIONS = new Set([".cjs", ".mjs", ".js", ".css", ".json", ".node", ".svg", ".wasm"]);
|
|
17
19
|
function publishedRuntimeFiles(root) {
|
|
@@ -39,6 +41,33 @@ function publishedRuntimeFiles(root) {
|
|
|
39
41
|
return files.sort((a, b) => a.localeCompare(b, "en"));
|
|
40
42
|
}
|
|
41
43
|
/** Same semver can contain a different local build, so hash every published shell runtime byte. */
|
|
44
|
+
/**
|
|
45
|
+
* The dashboard's CSRF capability, kept in an owned 0600 file so a page opened before a restart keeps working.
|
|
46
|
+
* A missing or unsafe file gets a new value; if it cannot be written, the value lasts for this process only.
|
|
47
|
+
*/
|
|
48
|
+
function loadDashboardCapability(file) {
|
|
49
|
+
try {
|
|
50
|
+
const stat = node_fs_1.default.lstatSync(file);
|
|
51
|
+
const value = node_fs_1.default.readFileSync(file, "utf8").trim();
|
|
52
|
+
if (stat.isFile() && (0, secure_metadata_js_1.exactMode)(stat, 0o600) && (typeof process.getuid !== "function" || stat.uid === process.getuid())
|
|
53
|
+
&& /^[A-Za-z0-9_-]{43}$/.test(value))
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
catch { /* missing or unreadable: issue a new one */ }
|
|
57
|
+
const value = node_crypto_1.default.randomBytes(32).toString("base64url");
|
|
58
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
59
|
+
try {
|
|
60
|
+
node_fs_1.default.writeFileSync(temporary, `${value}\n`, { mode: 0o600, flag: "wx" });
|
|
61
|
+
node_fs_1.default.renameSync(temporary, file);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
try {
|
|
65
|
+
node_fs_1.default.unlinkSync(temporary);
|
|
66
|
+
}
|
|
67
|
+
catch { /* nothing was written */ }
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
42
71
|
function dashboardBuildFingerprint(root) {
|
|
43
72
|
const hash = node_crypto_1.default.createHash("sha256");
|
|
44
73
|
for (const relative of publishedRuntimeFiles(root)) {
|