@yoooclaw/phone-notifications 1.12.3 → 1.12.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -4
- package/dist/bin/ntf.cjs +327 -9
- package/dist/bin/ntf.cjs.map +9 -7
- package/dist/cli/doctor/check-local-gateway.d.ts +3 -0
- package/dist/cli/doctor/check-local-gateway.d.ts.map +1 -0
- package/dist/cli/doctor/checkers.d.ts.map +1 -1
- package/dist/env.d.ts +8 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/fs-atomic.d.ts +11 -0
- package/dist/fs-atomic.d.ts.map +1 -0
- package/dist/image/store.d.ts.map +1 -1
- package/dist/index.cjs +2084 -740
- package/dist/index.cjs.map +34 -24
- package/dist/index.d.ts.map +1 -1
- package/dist/light/sender.d.ts +14 -3
- package/dist/light/sender.d.ts.map +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/notification/storage.d.ts +4 -3
- package/dist/notification/storage.d.ts.map +1 -1
- package/dist/plugin/auto-update.d.ts +2 -0
- package/dist/plugin/auto-update.d.ts.map +1 -1
- package/dist/plugin/lifecycle.d.ts +7 -14
- package/dist/plugin/lifecycle.d.ts.map +1 -1
- package/dist/plugin/light-control.d.ts.map +1 -1
- package/dist/plugin/light-rules-tools.d.ts.map +1 -1
- package/dist/plugin/notifications.d.ts +1 -0
- package/dist/plugin/notifications.d.ts.map +1 -1
- package/dist/plugin/register-guard.d.ts +16 -0
- package/dist/plugin/register-guard.d.ts.map +1 -0
- package/dist/plugin/runtime.d.ts +64 -0
- package/dist/plugin/runtime.d.ts.map +1 -0
- package/dist/plugin/shared-storage.d.ts +31 -0
- package/dist/plugin/shared-storage.d.ts.map +1 -0
- package/dist/plugin/system.d.ts +17 -0
- package/dist/plugin/system.d.ts.map +1 -0
- package/dist/recording/storage.d.ts +2 -1
- package/dist/recording/storage.d.ts.map +1 -1
- package/dist/system/os-info.d.ts +34 -0
- package/dist/system/os-info.d.ts.map +1 -0
- package/dist/tunnel/connect-status-reporter.d.ts.map +1 -1
- package/dist/tunnel/frame-slimmer.d.ts +82 -1
- package/dist/tunnel/frame-slimmer.d.ts.map +1 -1
- package/dist/tunnel/gateway-target.d.ts +17 -0
- package/dist/tunnel/gateway-target.d.ts.map +1 -0
- package/dist/tunnel/http-proxy.d.ts.map +1 -1
- package/dist/tunnel/proxy.d.ts +20 -1
- package/dist/tunnel/proxy.d.ts.map +1 -1
- package/dist/tunnel/service.d.ts.map +1 -1
- package/dist/update/executor.d.ts +3 -3
- package/dist/update/executor.d.ts.map +1 -1
- package/dist/update/index.d.ts +3 -2
- package/dist/update/index.d.ts.map +1 -1
- package/dist/update/stale-runtime.d.ts +43 -0
- package/dist/update/stale-runtime.d.ts.map +1 -0
- package/openclaw.plugin.json +1 -0
- package/package.json +1 -1
- package/skills/notification-monitor/SKILL.md +2 -2
- package/skills/notification-to-memory/SKILL.md +2 -2
package/dist/index.cjs
CHANGED
|
@@ -1719,7 +1719,7 @@ var require_websocket = __commonJS((exports2, module2) => {
|
|
|
1719
1719
|
var EventEmitter = require("events");
|
|
1720
1720
|
var https = require("https");
|
|
1721
1721
|
var http = require("http");
|
|
1722
|
-
var
|
|
1722
|
+
var net2 = require("net");
|
|
1723
1723
|
var tls = require("tls");
|
|
1724
1724
|
var { randomBytes: randomBytes2, createHash: createHash2 } = require("crypto");
|
|
1725
1725
|
var { Duplex, Readable: Readable2 } = require("stream");
|
|
@@ -2339,12 +2339,12 @@ var require_websocket = __commonJS((exports2, module2) => {
|
|
|
2339
2339
|
}
|
|
2340
2340
|
function netConnect(options) {
|
|
2341
2341
|
options.path = options.socketPath;
|
|
2342
|
-
return
|
|
2342
|
+
return net2.connect(options);
|
|
2343
2343
|
}
|
|
2344
2344
|
function tlsConnect(options) {
|
|
2345
2345
|
options.path = undefined;
|
|
2346
2346
|
if (!options.servername && options.servername !== "") {
|
|
2347
|
-
options.servername =
|
|
2347
|
+
options.servername = net2.isIP(options.host) ? "" : options.host;
|
|
2348
2348
|
}
|
|
2349
2349
|
return tls.connect(options);
|
|
2350
2350
|
}
|
|
@@ -2959,7 +2959,7 @@ var import_node_path = require("node:path");
|
|
|
2959
2959
|
var import_node_fs = require("node:fs");
|
|
2960
2960
|
function readBuildInjectedVersion() {
|
|
2961
2961
|
if (false) {}
|
|
2962
|
-
const version = "1.12.
|
|
2962
|
+
const version = "1.12.4".trim();
|
|
2963
2963
|
return version || undefined;
|
|
2964
2964
|
}
|
|
2965
2965
|
function readPluginVersionFromPackageJson() {
|
|
@@ -3086,7 +3086,6 @@ class PluginFileLogger {
|
|
|
3086
3086
|
this.lastPruneDateKey = formatDate(now);
|
|
3087
3087
|
}
|
|
3088
3088
|
info(msg) {
|
|
3089
|
-
this.prepareLogDate(new Date);
|
|
3090
3089
|
const safeMsg = this.formatLogMessage(msg);
|
|
3091
3090
|
this.upstream.info(safeMsg);
|
|
3092
3091
|
this.append("INFO", safeMsg);
|
|
@@ -3137,20 +3136,48 @@ class PluginFileLogger {
|
|
|
3137
3136
|
} catch {}
|
|
3138
3137
|
}
|
|
3139
3138
|
}
|
|
3139
|
+
function quotedObjectFieldRegex(keys) {
|
|
3140
|
+
return new RegExp(`(["'])(${buildKeyPattern(keys)})\\1\\s*:\\s*(?:"(?:\\\\.|[^"\\\\])*"|'(?:\\\\.|[^'\\\\])*'|[^,}\\]]+)`, "gi");
|
|
3141
|
+
}
|
|
3142
|
+
function keyValueFieldRegex(keys) {
|
|
3143
|
+
return new RegExp(`(?<![?&])\\b(${buildKeyPattern(keys)})\\s*=\\s*(?:Bearer\\s+[^,\\s)]+|"(?:\\\\.|[^"\\\\])*"|'(?:\\\\.|[^'\\\\])*'|[^,\\s)]+)`, "gi");
|
|
3144
|
+
}
|
|
3145
|
+
function structuredKeyValueFieldRegex(keys) {
|
|
3146
|
+
return new RegExp(`\\b(${buildKeyPattern(keys)})\\s*=\\s*([\\[{])`, "gi");
|
|
3147
|
+
}
|
|
3148
|
+
function colonTextFieldRegex(keys) {
|
|
3149
|
+
return new RegExp(`\\b(${buildKeyPattern(keys)})\\s*:\\s*([^,\\n]+)`, "gi");
|
|
3150
|
+
}
|
|
3151
|
+
function buildRules(build) {
|
|
3152
|
+
return [
|
|
3153
|
+
{ regex: build(SECRET_KEYS), placeholder: REDACTED_SECRET },
|
|
3154
|
+
{ regex: build(USER_TEXT_KEYS), placeholder: REDACTED },
|
|
3155
|
+
{ regex: build(USER_URL_KEYS), placeholder: REDACTED_URL }
|
|
3156
|
+
];
|
|
3157
|
+
}
|
|
3158
|
+
var QUOTED_OBJECT_FIELD_RULES = buildRules(quotedObjectFieldRegex);
|
|
3159
|
+
var KEY_VALUE_FIELD_RULES = buildRules(keyValueFieldRegex);
|
|
3160
|
+
var STRUCTURED_KEY_VALUE_FIELD_RULES = buildRules(structuredKeyValueFieldRegex);
|
|
3161
|
+
var COLON_TEXT_FIELD_RULES = [
|
|
3162
|
+
{ regex: colonTextFieldRegex(SECRET_KEYS), placeholder: REDACTED_SECRET },
|
|
3163
|
+
{ regex: colonTextFieldRegex(USER_TEXT_KEYS), placeholder: REDACTED }
|
|
3164
|
+
];
|
|
3165
|
+
var SENSITIVE_QUERY_PARAM_REGEX = new RegExp(`([?&](${SENSITIVE_QUERY_KEYS.map(escapeRegExp).join("|")})=)([^&#\\s]+)`, "gi");
|
|
3140
3166
|
function redactLogMessage(msg) {
|
|
3141
3167
|
let redacted = String(msg);
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3168
|
+
for (const rule of QUOTED_OBJECT_FIELD_RULES) {
|
|
3169
|
+
redacted = redactQuotedObjectFields(redacted, rule);
|
|
3170
|
+
}
|
|
3171
|
+
for (const rule of STRUCTURED_KEY_VALUE_FIELD_RULES) {
|
|
3172
|
+
redacted = redactStructuredKeyValueFields(redacted, rule);
|
|
3173
|
+
}
|
|
3174
|
+
for (const rule of KEY_VALUE_FIELD_RULES) {
|
|
3175
|
+
redacted = redactKeyValueFields(redacted, rule);
|
|
3176
|
+
}
|
|
3177
|
+
for (const rule of COLON_TEXT_FIELD_RULES) {
|
|
3178
|
+
redacted = redactColonTextFields(redacted, rule);
|
|
3179
|
+
}
|
|
3180
|
+
redacted = redactQueryParams(redacted);
|
|
3154
3181
|
redacted = redactBearerTokens(redacted);
|
|
3155
3182
|
redacted = redactLikelyUserUrls(redacted);
|
|
3156
3183
|
redacted = redactEmails(redacted);
|
|
@@ -3159,17 +3186,14 @@ function redactLogMessage(msg) {
|
|
|
3159
3186
|
redacted = redactLongHexTokens(redacted);
|
|
3160
3187
|
return redacted;
|
|
3161
3188
|
}
|
|
3162
|
-
function redactQuotedObjectFields(input,
|
|
3163
|
-
|
|
3164
|
-
return input.replace(new RegExp(`(["'])(${keyPattern})\\1\\s*:\\s*(?:"(?:\\\\.|[^"\\\\])*"|'(?:\\\\.|[^'\\\\])*'|[^,}\\]]+)`, "gi"), (_match, quote, key) => `${quote}${key}${quote}: "${placeholder}"`);
|
|
3189
|
+
function redactQuotedObjectFields(input, { regex, placeholder }) {
|
|
3190
|
+
return input.replace(regex, (_match, quote, key) => `${quote}${key}${quote}: "${placeholder}"`);
|
|
3165
3191
|
}
|
|
3166
|
-
function redactKeyValueFields(input,
|
|
3167
|
-
|
|
3168
|
-
return input.replace(new RegExp(`(?<![?&])\\b(${keyPattern})\\s*=\\s*(?:Bearer\\s+[^,\\s)]+|"(?:\\\\.|[^"\\\\])*"|'(?:\\\\.|[^'\\\\])*'|[^,\\s)]+)`, "gi"), (_match, key) => `${key}=${placeholder}`);
|
|
3192
|
+
function redactKeyValueFields(input, { regex, placeholder }) {
|
|
3193
|
+
return input.replace(regex, (_match, key) => `${key}=${placeholder}`);
|
|
3169
3194
|
}
|
|
3170
|
-
function redactStructuredKeyValueFields(input,
|
|
3171
|
-
|
|
3172
|
-
const regex = new RegExp(`\\b(${keyPattern})\\s*=\\s*([\\[{])`, "gi");
|
|
3195
|
+
function redactStructuredKeyValueFields(input, { regex, placeholder }) {
|
|
3196
|
+
regex.lastIndex = 0;
|
|
3173
3197
|
let result = "";
|
|
3174
3198
|
let lastIndex = 0;
|
|
3175
3199
|
let match;
|
|
@@ -3224,13 +3248,11 @@ function findStructuredValueEnd(input, start) {
|
|
|
3224
3248
|
}
|
|
3225
3249
|
return null;
|
|
3226
3250
|
}
|
|
3227
|
-
function redactColonTextFields(input,
|
|
3228
|
-
|
|
3229
|
-
return input.replace(new RegExp(`\\b(${keyPattern})\\s*:\\s*([^,\\n]+)`, "gi"), (_match, key) => `${key}: ${placeholder}`);
|
|
3251
|
+
function redactColonTextFields(input, { regex, placeholder }) {
|
|
3252
|
+
return input.replace(regex, (_match, key) => `${key}: ${placeholder}`);
|
|
3230
3253
|
}
|
|
3231
|
-
function redactQueryParams(input
|
|
3232
|
-
|
|
3233
|
-
return input.replace(new RegExp(`([?&](${keyPattern})=)([^&#\\s]+)`, "gi"), (_match, prefix) => `${prefix}${encodeURIComponent(REDACTED)}`);
|
|
3254
|
+
function redactQueryParams(input) {
|
|
3255
|
+
return input.replace(SENSITIVE_QUERY_PARAM_REGEX, (_match, prefix) => `${prefix}${encodeURIComponent(REDACTED)}`);
|
|
3234
3256
|
}
|
|
3235
3257
|
function redactBearerTokens(input) {
|
|
3236
3258
|
return input.replace(/\b(Bearer\s+)[A-Za-z0-9._~+/=-]+/gi, `$1${REDACTED_SECRET}`);
|
|
@@ -4036,9 +4058,28 @@ async function runNotificationSummary(params) {
|
|
|
4036
4058
|
}
|
|
4037
4059
|
|
|
4038
4060
|
// src/notification/storage.ts
|
|
4039
|
-
var
|
|
4061
|
+
var import_node_fs5 = require("node:fs");
|
|
4040
4062
|
var import_node_crypto = require("node:crypto");
|
|
4063
|
+
var import_node_path4 = require("node:path");
|
|
4064
|
+
|
|
4065
|
+
// src/fs-atomic.ts
|
|
4066
|
+
var import_node_fs4 = require("node:fs");
|
|
4041
4067
|
var import_node_path3 = require("node:path");
|
|
4068
|
+
function writeFileAtomic(path, data) {
|
|
4069
|
+
import_node_fs4.mkdirSync(import_node_path3.dirname(path), { recursive: true });
|
|
4070
|
+
const tmpPath = `${path}.tmp.${process.pid}.${Date.now()}`;
|
|
4071
|
+
import_node_fs4.writeFileSync(tmpPath, data, "utf-8");
|
|
4072
|
+
import_node_fs4.renameSync(tmpPath, path);
|
|
4073
|
+
}
|
|
4074
|
+
function quarantineCorruptFile(path) {
|
|
4075
|
+
const quarantinePath = `${path}.corrupt.${Date.now()}`;
|
|
4076
|
+
try {
|
|
4077
|
+
import_node_fs4.renameSync(path, quarantinePath);
|
|
4078
|
+
return quarantinePath;
|
|
4079
|
+
} catch {
|
|
4080
|
+
return null;
|
|
4081
|
+
}
|
|
4082
|
+
}
|
|
4042
4083
|
|
|
4043
4084
|
// src/notification/feishu-normalize.ts
|
|
4044
4085
|
function normalizeOptionalText(value) {
|
|
@@ -4180,7 +4221,7 @@ var NOTIFICATION_DIR_NAME = "notifications";
|
|
|
4180
4221
|
var ID_INDEX_DIR_NAME = ".ids";
|
|
4181
4222
|
var CONTENT_KEY_INDEX_DIR_NAME = ".keys";
|
|
4182
4223
|
function getStateFallbackNotificationDir(stateDir) {
|
|
4183
|
-
return
|
|
4224
|
+
return import_node_path4.join(stateDir, "plugins", "phone-notifications", NOTIFICATION_DIR_NAME);
|
|
4184
4225
|
}
|
|
4185
4226
|
function computeLagMs(timestamp) {
|
|
4186
4227
|
if (!timestamp)
|
|
@@ -4206,8 +4247,8 @@ function buildFallbackContent(n) {
|
|
|
4206
4247
|
}
|
|
4207
4248
|
function ensureWritableDirectory(dir) {
|
|
4208
4249
|
try {
|
|
4209
|
-
|
|
4210
|
-
|
|
4250
|
+
import_node_fs5.mkdirSync(dir, { recursive: true });
|
|
4251
|
+
import_node_fs5.accessSync(dir, import_node_fs5.constants.R_OK | import_node_fs5.constants.W_OK);
|
|
4211
4252
|
return true;
|
|
4212
4253
|
} catch {
|
|
4213
4254
|
return false;
|
|
@@ -4220,7 +4261,7 @@ function resolveNotificationStorageDir(ctx, logger) {
|
|
|
4220
4261
|
return stateNotifDir;
|
|
4221
4262
|
}
|
|
4222
4263
|
if (ctx.workspaceDir) {
|
|
4223
|
-
const workspaceDir =
|
|
4264
|
+
const workspaceDir = import_node_path4.join(ctx.workspaceDir, NOTIFICATION_DIR_NAME);
|
|
4224
4265
|
if (ensureWritableDirectory(workspaceDir)) {
|
|
4225
4266
|
logger.warn(`stateDir 不可用,通知已回退到 workspace 路径: ${workspaceDir}`);
|
|
4226
4267
|
return workspaceDir;
|
|
@@ -4228,6 +4269,7 @@ function resolveNotificationStorageDir(ctx, logger) {
|
|
|
4228
4269
|
}
|
|
4229
4270
|
throw new Error(`通知存储目录不可用: ${stateNotifDir}`);
|
|
4230
4271
|
}
|
|
4272
|
+
var DAY_ENTRIES_CACHE_MAX = 4;
|
|
4231
4273
|
|
|
4232
4274
|
class NotificationStorage {
|
|
4233
4275
|
logger;
|
|
@@ -4236,23 +4278,24 @@ class NotificationStorage {
|
|
|
4236
4278
|
contentKeyIndexDir;
|
|
4237
4279
|
idCache = new Map;
|
|
4238
4280
|
contentKeyCache = new Map;
|
|
4281
|
+
dayEntriesCache = new Map;
|
|
4239
4282
|
dateWriteChains = new Map;
|
|
4240
4283
|
resolveDisplayName;
|
|
4241
4284
|
constructor(dir, logger, resolveDisplayName) {
|
|
4242
4285
|
this.logger = logger;
|
|
4243
4286
|
this.dir = dir;
|
|
4244
|
-
this.idIndexDir =
|
|
4245
|
-
this.contentKeyIndexDir =
|
|
4287
|
+
this.idIndexDir = import_node_path4.join(dir, ID_INDEX_DIR_NAME);
|
|
4288
|
+
this.contentKeyIndexDir = import_node_path4.join(dir, CONTENT_KEY_INDEX_DIR_NAME);
|
|
4246
4289
|
this.resolveDisplayName = resolveDisplayName;
|
|
4247
4290
|
}
|
|
4248
4291
|
getStorageDir() {
|
|
4249
4292
|
return this.dir;
|
|
4250
4293
|
}
|
|
4251
4294
|
async init() {
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4295
|
+
import_node_fs5.mkdirSync(this.dir, { recursive: true });
|
|
4296
|
+
import_node_fs5.mkdirSync(this.idIndexDir, { recursive: true });
|
|
4297
|
+
import_node_fs5.rmSync(this.contentKeyIndexDir, { recursive: true, force: true });
|
|
4298
|
+
import_node_fs5.mkdirSync(this.contentKeyIndexDir, { recursive: true });
|
|
4256
4299
|
}
|
|
4257
4300
|
async ingest(items, ingestId) {
|
|
4258
4301
|
const result = {
|
|
@@ -4264,8 +4307,7 @@ class NotificationStorage {
|
|
|
4264
4307
|
inserted: []
|
|
4265
4308
|
};
|
|
4266
4309
|
const traceTag = ingestId ? `[${ingestId}]` : "";
|
|
4267
|
-
|
|
4268
|
-
const outcome = await this.writeNotification(n);
|
|
4310
|
+
const recordOutcome = (n, outcome) => {
|
|
4269
4311
|
const lagMs = computeLagMs(n.timestamp);
|
|
4270
4312
|
const lagPart = lagMs === null ? "lag=?" : `lag=${lagMs}ms`;
|
|
4271
4313
|
this.logger.info(`ingest${traceTag}: app=${n.app ?? "Unknown"} id=${n.id ?? "-"} ts=${n.timestamp} ${lagPart} outcome=${outcome.kind}`);
|
|
@@ -4284,41 +4326,108 @@ class NotificationStorage {
|
|
|
4284
4326
|
result.invalid += 1;
|
|
4285
4327
|
break;
|
|
4286
4328
|
}
|
|
4329
|
+
};
|
|
4330
|
+
const groups = new Map;
|
|
4331
|
+
for (const n of items) {
|
|
4332
|
+
const ts = new Date(n.timestamp);
|
|
4333
|
+
if (Number.isNaN(ts.getTime())) {
|
|
4334
|
+
this.logger.warn(`忽略非法 timestamp 的通知: ${n.id}`);
|
|
4335
|
+
recordOutcome(n, { kind: "invalid" });
|
|
4336
|
+
continue;
|
|
4337
|
+
}
|
|
4338
|
+
const dateKey = this.formatDate(ts);
|
|
4339
|
+
const group = groups.get(dateKey);
|
|
4340
|
+
if (group) {
|
|
4341
|
+
group.push(n);
|
|
4342
|
+
} else {
|
|
4343
|
+
groups.set(dateKey, [n]);
|
|
4344
|
+
}
|
|
4345
|
+
}
|
|
4346
|
+
for (const [dateKey, group] of groups) {
|
|
4347
|
+
await this.writeDayNotifications(dateKey, group, recordOutcome);
|
|
4287
4348
|
}
|
|
4288
4349
|
return result;
|
|
4289
4350
|
}
|
|
4290
|
-
async
|
|
4291
|
-
const
|
|
4292
|
-
|
|
4293
|
-
this.
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4351
|
+
async writeDayNotifications(dateKey, group, recordOutcome) {
|
|
4352
|
+
const filePath = import_node_path4.join(this.dir, `${dateKey}.json`);
|
|
4353
|
+
await this.withDateWriteLock(dateKey, async () => {
|
|
4354
|
+
const entries = this.getDayEntries(dateKey, filePath);
|
|
4355
|
+
const ids = this.getIdSet(dateKey);
|
|
4356
|
+
const contentKeys = this.getContentKeySet(dateKey, filePath);
|
|
4357
|
+
const pendingIdKeys = [];
|
|
4358
|
+
const pendingContentKeys = [];
|
|
4359
|
+
let insertedCount = 0;
|
|
4360
|
+
const outcomes = [];
|
|
4361
|
+
for (const n of group) {
|
|
4362
|
+
const normalizedId = typeof n.id === "string" ? n.id.trim() : "";
|
|
4363
|
+
const entry = this.buildStoredNotification(n);
|
|
4364
|
+
const clientLabel = entry.clientLabel ?? "legacy";
|
|
4365
|
+
if (normalizedId && this.hasNotificationId(dateKey, clientLabel, normalizedId)) {
|
|
4366
|
+
outcomes.push({ n, outcome: { kind: "dedupedById" } });
|
|
4367
|
+
continue;
|
|
4368
|
+
}
|
|
4369
|
+
if (this.hasNotificationContentKey(dateKey, filePath, entry)) {
|
|
4370
|
+
outcomes.push({ n, outcome: { kind: "dedupedByContent" } });
|
|
4371
|
+
continue;
|
|
4372
|
+
}
|
|
4373
|
+
let appDisplayName = entry.appName;
|
|
4374
|
+
if (this.resolveDisplayName) {
|
|
4375
|
+
try {
|
|
4376
|
+
appDisplayName = await this.resolveDisplayName(entry.appName);
|
|
4377
|
+
} catch {
|
|
4378
|
+
appDisplayName = entry.appName;
|
|
4379
|
+
}
|
|
4380
|
+
}
|
|
4381
|
+
const storedEntry = { ...entry, appDisplayName };
|
|
4382
|
+
entries.push(storedEntry);
|
|
4383
|
+
insertedCount += 1;
|
|
4384
|
+
if (normalizedId) {
|
|
4385
|
+
const idKey = this.buildNotificationIdKey(clientLabel, normalizedId);
|
|
4386
|
+
if (!ids.has(idKey)) {
|
|
4387
|
+
ids.add(idKey);
|
|
4388
|
+
pendingIdKeys.push(idKey);
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4391
|
+
const contentKey = this.buildNotificationContentKey(storedEntry);
|
|
4392
|
+
if (!contentKeys.has(contentKey)) {
|
|
4393
|
+
contentKeys.add(contentKey);
|
|
4394
|
+
pendingContentKeys.push(contentKey);
|
|
4395
|
+
}
|
|
4396
|
+
outcomes.push({ n, outcome: { kind: "ingested", entry: storedEntry } });
|
|
4397
|
+
}
|
|
4398
|
+
if (insertedCount > 0) {
|
|
4399
|
+
writeFileAtomic(filePath, JSON.stringify(entries, null, 2));
|
|
4400
|
+
if (pendingIdKeys.length > 0) {
|
|
4401
|
+
import_node_fs5.appendFileSync(this.getIdIndexPath(dateKey), `${pendingIdKeys.join(`
|
|
4402
|
+
`)}
|
|
4403
|
+
`, "utf-8");
|
|
4404
|
+
}
|
|
4405
|
+
if (pendingContentKeys.length > 0) {
|
|
4406
|
+
import_node_fs5.appendFileSync(this.getContentKeyIndexPath(dateKey), `${pendingContentKeys.join(`
|
|
4407
|
+
`)}
|
|
4408
|
+
`, "utf-8");
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
for (const { n, outcome } of outcomes) {
|
|
4412
|
+
recordOutcome(n, outcome);
|
|
4413
|
+
}
|
|
4320
4414
|
});
|
|
4321
4415
|
}
|
|
4416
|
+
getDayEntries(dateKey, filePath) {
|
|
4417
|
+
const cached = this.dayEntriesCache.get(dateKey);
|
|
4418
|
+
if (cached) {
|
|
4419
|
+
return cached;
|
|
4420
|
+
}
|
|
4421
|
+
const entries = this.readStoredNotifications(filePath);
|
|
4422
|
+
this.dayEntriesCache.set(dateKey, entries);
|
|
4423
|
+
while (this.dayEntriesCache.size > DAY_ENTRIES_CACHE_MAX) {
|
|
4424
|
+
const oldest = this.dayEntriesCache.keys().next().value;
|
|
4425
|
+
if (oldest === undefined)
|
|
4426
|
+
break;
|
|
4427
|
+
this.dayEntriesCache.delete(oldest);
|
|
4428
|
+
}
|
|
4429
|
+
return entries;
|
|
4430
|
+
}
|
|
4322
4431
|
buildStoredNotification(n) {
|
|
4323
4432
|
const appName = typeof n.app === "string" && n.app ? n.app : "Unknown";
|
|
4324
4433
|
const clientLabel = currentClientLabel("default");
|
|
@@ -4348,7 +4457,7 @@ class NotificationStorage {
|
|
|
4348
4457
|
return `${year}-${month}-${day}`;
|
|
4349
4458
|
}
|
|
4350
4459
|
getIdIndexPath(dateKey) {
|
|
4351
|
-
return
|
|
4460
|
+
return import_node_path4.join(this.idIndexDir, `${dateKey}.ids`);
|
|
4352
4461
|
}
|
|
4353
4462
|
getIdSet(dateKey) {
|
|
4354
4463
|
const cached = this.idCache.get(dateKey);
|
|
@@ -4357,8 +4466,8 @@ class NotificationStorage {
|
|
|
4357
4466
|
}
|
|
4358
4467
|
const idPath = this.getIdIndexPath(dateKey);
|
|
4359
4468
|
const ids = new Set;
|
|
4360
|
-
if (
|
|
4361
|
-
const lines =
|
|
4469
|
+
if (import_node_fs5.existsSync(idPath)) {
|
|
4470
|
+
const lines = import_node_fs5.readFileSync(idPath, "utf-8").split(/\r?\n/);
|
|
4362
4471
|
for (const line of lines) {
|
|
4363
4472
|
const id = line.trim();
|
|
4364
4473
|
if (id) {
|
|
@@ -4383,7 +4492,7 @@ class NotificationStorage {
|
|
|
4383
4492
|
return ids.has(this.buildNotificationIdKey(clientLabel, id)) || this.usesLegacyIdKey(clientLabel) && ids.has(id);
|
|
4384
4493
|
}
|
|
4385
4494
|
getContentKeyIndexPath(dateKey) {
|
|
4386
|
-
return
|
|
4495
|
+
return import_node_path4.join(this.contentKeyIndexDir, `${dateKey}.keys`);
|
|
4387
4496
|
}
|
|
4388
4497
|
getContentKeySet(dateKey, filePath) {
|
|
4389
4498
|
const cached = this.contentKeyCache.get(dateKey);
|
|
@@ -4392,17 +4501,15 @@ class NotificationStorage {
|
|
|
4392
4501
|
}
|
|
4393
4502
|
const keyPath = this.getContentKeyIndexPath(dateKey);
|
|
4394
4503
|
const keys = new Set;
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
keys.add(this.buildNotificationContentKey(item));
|
|
4398
|
-
}
|
|
4504
|
+
for (const item of this.getDayEntries(dateKey, filePath)) {
|
|
4505
|
+
keys.add(this.buildNotificationContentKey(item));
|
|
4399
4506
|
}
|
|
4400
4507
|
if (keys.size > 0) {
|
|
4401
|
-
|
|
4508
|
+
writeFileAtomic(keyPath, `${Array.from(keys).join(`
|
|
4402
4509
|
`)}
|
|
4403
|
-
|
|
4404
|
-
} else if (
|
|
4405
|
-
|
|
4510
|
+
`);
|
|
4511
|
+
} else if (import_node_fs5.existsSync(keyPath)) {
|
|
4512
|
+
import_node_fs5.rmSync(keyPath, { force: true });
|
|
4406
4513
|
}
|
|
4407
4514
|
this.contentKeyCache.set(dateKey, keys);
|
|
4408
4515
|
return keys;
|
|
@@ -4411,26 +4518,6 @@ class NotificationStorage {
|
|
|
4411
4518
|
const keys = this.getContentKeySet(dateKey, filePath);
|
|
4412
4519
|
return this.contentKeyLabels(entry.clientLabel).some((clientLabel) => keys.has(this.buildNotificationContentKey({ ...entry, clientLabel })));
|
|
4413
4520
|
}
|
|
4414
|
-
recordNotificationId(dateKey, clientLabel, id) {
|
|
4415
|
-
const ids = this.getIdSet(dateKey);
|
|
4416
|
-
const key = this.buildNotificationIdKey(clientLabel, id);
|
|
4417
|
-
if (ids.has(key)) {
|
|
4418
|
-
return;
|
|
4419
|
-
}
|
|
4420
|
-
import_node_fs4.appendFileSync(this.getIdIndexPath(dateKey), `${key}
|
|
4421
|
-
`, "utf-8");
|
|
4422
|
-
ids.add(key);
|
|
4423
|
-
}
|
|
4424
|
-
recordNotificationContentKey(dateKey, filePath, entry) {
|
|
4425
|
-
const keys = this.getContentKeySet(dateKey, filePath);
|
|
4426
|
-
const key = this.buildNotificationContentKey(entry);
|
|
4427
|
-
if (keys.has(key)) {
|
|
4428
|
-
return;
|
|
4429
|
-
}
|
|
4430
|
-
import_node_fs4.appendFileSync(this.getContentKeyIndexPath(dateKey), `${key}
|
|
4431
|
-
`, "utf-8");
|
|
4432
|
-
keys.add(key);
|
|
4433
|
-
}
|
|
4434
4521
|
buildNotificationContentKey(entry) {
|
|
4435
4522
|
return import_node_crypto.createHash("sha256").update(entry.clientLabel ?? "legacy").update("\x1F").update(entry.appName).update("\x1F").update(entry.title).update("\x1F").update(entry.content).update("\x1F").update(entry.timestamp).digest("hex");
|
|
4436
4523
|
}
|
|
@@ -4439,13 +4526,15 @@ class NotificationStorage {
|
|
|
4439
4526
|
return label === "default" ? ["default", "legacy"] : [label];
|
|
4440
4527
|
}
|
|
4441
4528
|
readStoredNotifications(filePath) {
|
|
4442
|
-
if (!
|
|
4529
|
+
if (!import_node_fs5.existsSync(filePath)) {
|
|
4443
4530
|
return [];
|
|
4444
4531
|
}
|
|
4445
4532
|
try {
|
|
4446
|
-
const parsed = JSON.parse(
|
|
4533
|
+
const parsed = JSON.parse(import_node_fs5.readFileSync(filePath, "utf-8"));
|
|
4447
4534
|
return Array.isArray(parsed) ? parsed : [];
|
|
4448
4535
|
} catch {
|
|
4536
|
+
const quarantinePath = quarantineCorruptFile(filePath);
|
|
4537
|
+
this.logger.warn(quarantinePath ? `通知文件损坏,已隔离到 ${quarantinePath},当日通知从空文件重新累积` : `通知文件损坏且隔离失败,后续写入可能覆盖原文件: ${filePath}`);
|
|
4449
4538
|
return [];
|
|
4450
4539
|
}
|
|
4451
4540
|
}
|
|
@@ -4470,6 +4559,7 @@ class NotificationStorage {
|
|
|
4470
4559
|
async close() {
|
|
4471
4560
|
this.idCache.clear();
|
|
4472
4561
|
this.contentKeyCache.clear();
|
|
4562
|
+
this.dayEntriesCache.clear();
|
|
4473
4563
|
this.dateWriteChains.clear();
|
|
4474
4564
|
}
|
|
4475
4565
|
}
|
|
@@ -4790,31 +4880,31 @@ var LIGHT_RULE_TOOL_NAME_LIST = [
|
|
|
4790
4880
|
];
|
|
4791
4881
|
|
|
4792
4882
|
// src/light-rules/storage.ts
|
|
4793
|
-
var
|
|
4794
|
-
var
|
|
4883
|
+
var import_node_fs6 = require("node:fs");
|
|
4884
|
+
var import_node_path5 = require("node:path");
|
|
4795
4885
|
function addUniquePath(paths, path) {
|
|
4796
4886
|
if (!path)
|
|
4797
4887
|
return;
|
|
4798
|
-
const normalized =
|
|
4888
|
+
const normalized = import_node_path5.resolve(path);
|
|
4799
4889
|
if (!paths.includes(normalized)) {
|
|
4800
4890
|
paths.push(normalized);
|
|
4801
4891
|
}
|
|
4802
4892
|
}
|
|
4803
4893
|
function inferSiblingWorkspaceDir(stateDir) {
|
|
4804
|
-
const stateDirName =
|
|
4894
|
+
const stateDirName = import_node_path5.basename(stateDir);
|
|
4805
4895
|
if (!stateDirName.startsWith("."))
|
|
4806
4896
|
return;
|
|
4807
|
-
const siblingStateDir =
|
|
4808
|
-
const siblingWorkspaceDir =
|
|
4809
|
-
return
|
|
4897
|
+
const siblingStateDir = import_node_path5.join(import_node_path5.dirname(stateDir), stateDirName.slice(1));
|
|
4898
|
+
const siblingWorkspaceDir = import_node_path5.join(siblingStateDir, "workspace");
|
|
4899
|
+
return import_node_fs6.existsSync(siblingWorkspaceDir) ? siblingWorkspaceDir : undefined;
|
|
4810
4900
|
}
|
|
4811
4901
|
function resolveBaseDirs(ctx) {
|
|
4812
4902
|
const dirs = [];
|
|
4813
4903
|
addUniquePath(dirs, ctx.workspaceDir);
|
|
4814
4904
|
if (ctx.stateDir) {
|
|
4815
4905
|
addUniquePath(dirs, inferSiblingWorkspaceDir(ctx.stateDir));
|
|
4816
|
-
const inferredWorkspaceDir =
|
|
4817
|
-
if (
|
|
4906
|
+
const inferredWorkspaceDir = import_node_path5.join(ctx.stateDir, "workspace");
|
|
4907
|
+
if (import_node_fs6.existsSync(inferredWorkspaceDir)) {
|
|
4818
4908
|
addUniquePath(dirs, inferredWorkspaceDir);
|
|
4819
4909
|
}
|
|
4820
4910
|
addUniquePath(dirs, ctx.stateDir);
|
|
@@ -4828,10 +4918,10 @@ function resolveWriteBaseDir(ctx) {
|
|
|
4828
4918
|
return resolveBaseDirs(ctx)[0];
|
|
4829
4919
|
}
|
|
4830
4920
|
function tasksDirs(ctx) {
|
|
4831
|
-
return resolveBaseDirs(ctx).map((baseDir) =>
|
|
4921
|
+
return resolveBaseDirs(ctx).map((baseDir) => import_node_path5.join(baseDir, "tasks"));
|
|
4832
4922
|
}
|
|
4833
4923
|
function writeTasksDir(ctx) {
|
|
4834
|
-
return
|
|
4924
|
+
return import_node_path5.join(resolveWriteBaseDir(ctx), "tasks");
|
|
4835
4925
|
}
|
|
4836
4926
|
function normalizeLightRuleLookupName(name) {
|
|
4837
4927
|
return name.trim().replace(/\.json$/i, "");
|
|
@@ -4842,7 +4932,7 @@ function resolveLightRuleTask(ctx, name) {
|
|
|
4842
4932
|
return null;
|
|
4843
4933
|
const dirs = tasksDirs(ctx);
|
|
4844
4934
|
for (const dir of dirs) {
|
|
4845
|
-
const directTaskDir =
|
|
4935
|
+
const directTaskDir = import_node_path5.join(dir, normalizedName);
|
|
4846
4936
|
const directMeta = readMeta(directTaskDir);
|
|
4847
4937
|
if (directMeta) {
|
|
4848
4938
|
return {
|
|
@@ -4852,12 +4942,12 @@ function resolveLightRuleTask(ctx, name) {
|
|
|
4852
4942
|
}
|
|
4853
4943
|
}
|
|
4854
4944
|
for (const dir of dirs) {
|
|
4855
|
-
if (!
|
|
4945
|
+
if (!import_node_fs6.existsSync(dir))
|
|
4856
4946
|
continue;
|
|
4857
|
-
for (const entry of
|
|
4947
|
+
for (const entry of import_node_fs6.readdirSync(dir, { withFileTypes: true })) {
|
|
4858
4948
|
if (!entry.isDirectory())
|
|
4859
4949
|
continue;
|
|
4860
|
-
const taskDir =
|
|
4950
|
+
const taskDir = import_node_path5.join(dir, entry.name);
|
|
4861
4951
|
const meta = readMeta(taskDir);
|
|
4862
4952
|
if (meta?.name === normalizedName) {
|
|
4863
4953
|
return {
|
|
@@ -4876,21 +4966,21 @@ function readOptionalString(value) {
|
|
|
4876
4966
|
return trimmed || undefined;
|
|
4877
4967
|
}
|
|
4878
4968
|
function readMeta(taskDir) {
|
|
4879
|
-
const metaPath =
|
|
4880
|
-
if (!
|
|
4969
|
+
const metaPath = import_node_path5.join(taskDir, "meta.json");
|
|
4970
|
+
if (!import_node_fs6.existsSync(metaPath))
|
|
4881
4971
|
return null;
|
|
4882
4972
|
try {
|
|
4883
|
-
const raw = JSON.parse(
|
|
4973
|
+
const raw = JSON.parse(import_node_fs6.readFileSync(metaPath, "utf-8"));
|
|
4884
4974
|
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
4885
4975
|
return null;
|
|
4886
4976
|
if (raw.type !== "light-rule")
|
|
4887
4977
|
return null;
|
|
4888
4978
|
if (!Array.isArray(raw.segments))
|
|
4889
4979
|
return null;
|
|
4890
|
-
const name = readOptionalString(raw.name) ??
|
|
4980
|
+
const name = readOptionalString(raw.name) ?? import_node_path5.basename(taskDir);
|
|
4891
4981
|
const title = readOptionalString(raw.title) ?? name;
|
|
4892
4982
|
const description = readOptionalString(raw.description) ?? name;
|
|
4893
|
-
const createdAt = readOptionalString(raw.createdAt) ??
|
|
4983
|
+
const createdAt = readOptionalString(raw.createdAt) ?? import_node_fs6.statSync(metaPath).birthtime.toISOString();
|
|
4894
4984
|
const enabled = typeof raw.enabled === "boolean" ? raw.enabled : true;
|
|
4895
4985
|
const repeatTimes = normalizeRepeatTimes({
|
|
4896
4986
|
repeat: raw.repeat,
|
|
@@ -4913,18 +5003,18 @@ function readMeta(taskDir) {
|
|
|
4913
5003
|
}
|
|
4914
5004
|
}
|
|
4915
5005
|
function writeMeta(taskDir, meta) {
|
|
4916
|
-
|
|
5006
|
+
import_node_fs6.writeFileSync(import_node_path5.join(taskDir, "meta.json"), JSON.stringify(meta, null, 2), "utf-8");
|
|
4917
5007
|
}
|
|
4918
5008
|
function listLightRules(ctx) {
|
|
4919
5009
|
const rules = [];
|
|
4920
5010
|
const seenNames = new Set;
|
|
4921
5011
|
for (const dir of tasksDirs(ctx)) {
|
|
4922
|
-
if (!
|
|
5012
|
+
if (!import_node_fs6.existsSync(dir))
|
|
4923
5013
|
continue;
|
|
4924
|
-
for (const entry of
|
|
5014
|
+
for (const entry of import_node_fs6.readdirSync(dir, { withFileTypes: true })) {
|
|
4925
5015
|
if (!entry.isDirectory())
|
|
4926
5016
|
continue;
|
|
4927
|
-
const meta = readMeta(
|
|
5017
|
+
const meta = readMeta(import_node_path5.join(dir, entry.name));
|
|
4928
5018
|
if (!meta || seenNames.has(meta.name))
|
|
4929
5019
|
continue;
|
|
4930
5020
|
rules.push(meta);
|
|
@@ -4939,11 +5029,11 @@ function createLightRule(ctx, params) {
|
|
|
4939
5029
|
throw new LightRuleError("ALREADY_EXISTS", `灯效规则 '${params.name}' 已存在`);
|
|
4940
5030
|
}
|
|
4941
5031
|
const dir = writeTasksDir(ctx);
|
|
4942
|
-
const taskDir =
|
|
4943
|
-
if (
|
|
5032
|
+
const taskDir = import_node_path5.join(dir, params.name);
|
|
5033
|
+
if (import_node_fs6.existsSync(taskDir)) {
|
|
4944
5034
|
throw new LightRuleError("ALREADY_EXISTS", `灯效规则 '${params.name}' 已存在`);
|
|
4945
5035
|
}
|
|
4946
|
-
|
|
5036
|
+
import_node_fs6.mkdirSync(taskDir, { recursive: true });
|
|
4947
5037
|
const repeatTimes = normalizeRepeatTimes({
|
|
4948
5038
|
repeat: params.repeat,
|
|
4949
5039
|
repeat_times: params.repeat_times
|
|
@@ -4999,7 +5089,7 @@ function deleteLightRule(ctx, name) {
|
|
|
4999
5089
|
if (!taskDir || !meta) {
|
|
5000
5090
|
throw new LightRuleError("NOT_FOUND", `灯效规则 '${name}' 不存在`);
|
|
5001
5091
|
}
|
|
5002
|
-
|
|
5092
|
+
import_node_fs6.rmSync(taskDir, { recursive: true, force: true });
|
|
5003
5093
|
return { name: meta.name };
|
|
5004
5094
|
}
|
|
5005
5095
|
|
|
@@ -5435,16 +5525,16 @@ function quantizeWindow(value) {
|
|
|
5435
5525
|
}
|
|
5436
5526
|
|
|
5437
5527
|
// src/auth/credentials.ts
|
|
5438
|
-
var
|
|
5439
|
-
var
|
|
5528
|
+
var import_node_fs9 = require("node:fs");
|
|
5529
|
+
var import_node_path9 = require("node:path");
|
|
5440
5530
|
|
|
5441
5531
|
// src/profile/detect.ts
|
|
5442
|
-
var
|
|
5443
|
-
var
|
|
5532
|
+
var import_node_fs8 = require("node:fs");
|
|
5533
|
+
var import_node_path7 = require("node:path");
|
|
5444
5534
|
|
|
5445
5535
|
// src/profile/paths.ts
|
|
5446
|
-
var
|
|
5447
|
-
var
|
|
5536
|
+
var import_node_fs7 = require("node:fs");
|
|
5537
|
+
var import_node_path6 = require("node:path");
|
|
5448
5538
|
var DEFAULT_JVSCLAW_STATE_DIR = "/home/admin/.openclaw";
|
|
5449
5539
|
var PHONE_NOTIFICATIONS_STATE_DIR_ENV = "PHONE_NOTIFICATIONS_STATE_DIR";
|
|
5450
5540
|
var PHONE_NOTIFICATIONS_CONFIG_PATH_ENV = "PHONE_NOTIFICATIONS_CONFIG_PATH";
|
|
@@ -5466,24 +5556,24 @@ function expandUserPath(value) {
|
|
|
5466
5556
|
return homeDir();
|
|
5467
5557
|
}
|
|
5468
5558
|
if (value.startsWith("~/")) {
|
|
5469
|
-
return
|
|
5559
|
+
return import_node_path6.join(homeDir(), value.slice(2));
|
|
5470
5560
|
}
|
|
5471
5561
|
return value;
|
|
5472
5562
|
}
|
|
5473
5563
|
function candidateMetaPaths() {
|
|
5474
5564
|
const home = homeDir();
|
|
5475
5565
|
return [
|
|
5476
|
-
|
|
5477
|
-
|
|
5566
|
+
import_node_path6.join(home, ".qclaw", "qclaw.json"),
|
|
5567
|
+
import_node_path6.join(home, ".qclow", "qclaw.json")
|
|
5478
5568
|
];
|
|
5479
5569
|
}
|
|
5480
5570
|
function loadQClawMeta() {
|
|
5481
5571
|
for (const metaPath of candidateMetaPaths()) {
|
|
5482
|
-
if (!
|
|
5572
|
+
if (!import_node_fs7.existsSync(metaPath)) {
|
|
5483
5573
|
continue;
|
|
5484
5574
|
}
|
|
5485
5575
|
try {
|
|
5486
|
-
const parsed = JSON.parse(
|
|
5576
|
+
const parsed = JSON.parse(import_node_fs7.readFileSync(metaPath, "utf-8"));
|
|
5487
5577
|
return {
|
|
5488
5578
|
stateDir: expandUserPath(trimToUndefined(parsed?.stateDir)),
|
|
5489
5579
|
configPath: expandUserPath(trimToUndefined(parsed?.configPath))
|
|
@@ -5505,12 +5595,12 @@ function resolveConfigPathFromEnv() {
|
|
|
5505
5595
|
return expandUserPath(trimToUndefined(process.env.JVSCLAW_CONFIG_PATH) ?? trimToUndefined(process.env.OPENCLAW_CONFIG_PATH) ?? trimToUndefined(process.env.QCLAW_CONFIG_PATH));
|
|
5506
5596
|
}
|
|
5507
5597
|
function hasOpenClawMarkers() {
|
|
5508
|
-
const baseDir =
|
|
5598
|
+
const baseDir = import_node_path6.join(homeDir(), ".openclaw");
|
|
5509
5599
|
return [
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
].some((candidate) =>
|
|
5600
|
+
import_node_path6.join(baseDir, "openclaw.json"),
|
|
5601
|
+
import_node_path6.join(baseDir, "credentials.json"),
|
|
5602
|
+
import_node_path6.join(baseDir, "extensions")
|
|
5603
|
+
].some((candidate) => import_node_fs7.existsSync(candidate));
|
|
5514
5604
|
}
|
|
5515
5605
|
function resolveHostStateDir(stateDir) {
|
|
5516
5606
|
const explicitDir = expandUserPath(trimToUndefined(stateDir));
|
|
@@ -5522,16 +5612,16 @@ function resolveHostStateDir(stateDir) {
|
|
|
5522
5612
|
return envDir;
|
|
5523
5613
|
}
|
|
5524
5614
|
if (hasOpenClawMarkers()) {
|
|
5525
|
-
return
|
|
5615
|
+
return import_node_path6.join(homeDir(), ".openclaw");
|
|
5526
5616
|
}
|
|
5527
5617
|
const meta = loadQClawMeta();
|
|
5528
5618
|
if (meta?.stateDir) {
|
|
5529
5619
|
return meta.stateDir;
|
|
5530
5620
|
}
|
|
5531
5621
|
if (meta?.configPath) {
|
|
5532
|
-
return
|
|
5622
|
+
return import_node_path6.dirname(meta.configPath);
|
|
5533
5623
|
}
|
|
5534
|
-
return
|
|
5624
|
+
return import_node_path6.join(homeDir(), ".openclaw");
|
|
5535
5625
|
}
|
|
5536
5626
|
function resolveHostConfigPath(stateDir = resolveHostStateDir()) {
|
|
5537
5627
|
const envConfigPath = resolveConfigPathFromEnv();
|
|
@@ -5542,7 +5632,7 @@ function resolveHostConfigPath(stateDir = resolveHostStateDir()) {
|
|
|
5542
5632
|
if (meta?.configPath && (!meta.stateDir || !stateDir || meta.stateDir === stateDir)) {
|
|
5543
5633
|
return meta.configPath;
|
|
5544
5634
|
}
|
|
5545
|
-
return
|
|
5635
|
+
return import_node_path6.join(stateDir, "openclaw.json");
|
|
5546
5636
|
}
|
|
5547
5637
|
function resolvePersistedProfileStateDirs() {
|
|
5548
5638
|
const dirs = [];
|
|
@@ -5597,7 +5687,7 @@ function readPersistedClawProfile() {
|
|
|
5597
5687
|
for (const stateDir of resolvePersistedProfileStateDirs()) {
|
|
5598
5688
|
let content;
|
|
5599
5689
|
try {
|
|
5600
|
-
content =
|
|
5690
|
+
content = import_node_fs8.readFileSync(import_node_path7.join(stateDir, ".env"), "utf-8");
|
|
5601
5691
|
} catch {
|
|
5602
5692
|
continue;
|
|
5603
5693
|
}
|
|
@@ -5621,7 +5711,7 @@ function readPluginLinkSecret(configPath) {
|
|
|
5621
5711
|
return;
|
|
5622
5712
|
let config;
|
|
5623
5713
|
try {
|
|
5624
|
-
config = JSON.parse(
|
|
5714
|
+
config = JSON.parse(import_node_fs8.readFileSync(configPath, "utf-8"));
|
|
5625
5715
|
} catch {
|
|
5626
5716
|
return;
|
|
5627
5717
|
}
|
|
@@ -5635,7 +5725,7 @@ function hasJvsclawLinkSecret() {
|
|
|
5635
5725
|
const paths = [
|
|
5636
5726
|
resolvePluginConfigPathOverrideFromEnv(),
|
|
5637
5727
|
resolveConfigPathFromEnv(),
|
|
5638
|
-
|
|
5728
|
+
import_node_path7.join(DEFAULT_JVSCLAW_STATE_DIR, "openclaw.json")
|
|
5639
5729
|
].filter((value, index, arr) => Boolean(value) && arr.indexOf(value) === index);
|
|
5640
5730
|
return paths.some((configPath) => Boolean(readPluginLinkSecret(configPath)));
|
|
5641
5731
|
}
|
|
@@ -5661,7 +5751,7 @@ function resolveClawKind() {
|
|
|
5661
5751
|
return "openclaw";
|
|
5662
5752
|
}
|
|
5663
5753
|
// src/profile/runtime-paths.ts
|
|
5664
|
-
var
|
|
5754
|
+
var import_node_path8 = require("node:path");
|
|
5665
5755
|
function resolveStateDir(hostStateDir) {
|
|
5666
5756
|
const override = resolvePluginStateDirOverrideFromEnv();
|
|
5667
5757
|
if (override) {
|
|
@@ -5678,12 +5768,12 @@ function resolveConfigPath(hostStateDir) {
|
|
|
5678
5768
|
return override;
|
|
5679
5769
|
}
|
|
5680
5770
|
if (resolveClawKind() === "jvsclaw") {
|
|
5681
|
-
return
|
|
5771
|
+
return import_node_path8.join(resolveStateDir(hostStateDir), "openclaw.json");
|
|
5682
5772
|
}
|
|
5683
5773
|
return resolveHostConfigPath(resolveHostStateDir(hostStateDir));
|
|
5684
5774
|
}
|
|
5685
5775
|
function resolveStateFile(filename, hostStateDir) {
|
|
5686
|
-
return
|
|
5776
|
+
return import_node_path8.join(resolveStateDir(hostStateDir), filename);
|
|
5687
5777
|
}
|
|
5688
5778
|
// src/auth/credentials.ts
|
|
5689
5779
|
var API_KEY_ENV = "YOOOCLAW_API_KEY";
|
|
@@ -5692,18 +5782,18 @@ function credentialsPath() {
|
|
|
5692
5782
|
}
|
|
5693
5783
|
function readCredentials() {
|
|
5694
5784
|
const path = credentialsPath();
|
|
5695
|
-
if (!
|
|
5785
|
+
if (!import_node_fs9.existsSync(path))
|
|
5696
5786
|
return {};
|
|
5697
5787
|
try {
|
|
5698
|
-
return JSON.parse(
|
|
5788
|
+
return JSON.parse(import_node_fs9.readFileSync(path, "utf-8"));
|
|
5699
5789
|
} catch {
|
|
5700
5790
|
return {};
|
|
5701
5791
|
}
|
|
5702
5792
|
}
|
|
5703
5793
|
function writeCredentials(creds) {
|
|
5704
5794
|
const path = credentialsPath();
|
|
5705
|
-
|
|
5706
|
-
|
|
5795
|
+
import_node_fs9.mkdirSync(import_node_path9.dirname(path), { recursive: true, mode: 448 });
|
|
5796
|
+
import_node_fs9.writeFileSync(path, JSON.stringify(creds, null, 2), {
|
|
5707
5797
|
encoding: "utf-8",
|
|
5708
5798
|
mode: 384
|
|
5709
5799
|
});
|
|
@@ -5751,8 +5841,8 @@ function requireApiKey() {
|
|
|
5751
5841
|
}
|
|
5752
5842
|
function watchCredentials(onChange) {
|
|
5753
5843
|
const path = credentialsPath();
|
|
5754
|
-
const dir =
|
|
5755
|
-
const filename =
|
|
5844
|
+
const dir = import_node_path9.dirname(path);
|
|
5845
|
+
const filename = import_node_path9.basename(path);
|
|
5756
5846
|
let debounceTimer = null;
|
|
5757
5847
|
const delayMs = 200;
|
|
5758
5848
|
const listener = (_event, changedName) => {
|
|
@@ -5767,7 +5857,7 @@ function watchCredentials(onChange) {
|
|
|
5767
5857
|
};
|
|
5768
5858
|
let watcher = null;
|
|
5769
5859
|
try {
|
|
5770
|
-
watcher =
|
|
5860
|
+
watcher = import_node_fs9.watch(dir, { persistent: false }, listener);
|
|
5771
5861
|
} catch {}
|
|
5772
5862
|
return () => {
|
|
5773
5863
|
if (debounceTimer)
|
|
@@ -5778,8 +5868,8 @@ function watchCredentials(onChange) {
|
|
|
5778
5868
|
|
|
5779
5869
|
// src/env.ts
|
|
5780
5870
|
var import_node_crypto2 = require("node:crypto");
|
|
5781
|
-
var
|
|
5782
|
-
var
|
|
5871
|
+
var import_node_fs10 = require("node:fs");
|
|
5872
|
+
var import_node_path10 = require("node:path");
|
|
5783
5873
|
function parseEnvContent(content) {
|
|
5784
5874
|
const entries = new Map;
|
|
5785
5875
|
const tokens = content.split(/\r?\n/).flatMap((line) => line.split(/(?<![A-Z0-9_])(?=[A-Z_][A-Z0-9_]*=)/));
|
|
@@ -5799,8 +5889,8 @@ function parseEnvContent(content) {
|
|
|
5799
5889
|
}
|
|
5800
5890
|
function writeDotEnv(key, value) {
|
|
5801
5891
|
const path = resolveStateFile(".env");
|
|
5802
|
-
|
|
5803
|
-
const existing =
|
|
5892
|
+
import_node_fs10.mkdirSync(import_node_path10.dirname(path), { recursive: true });
|
|
5893
|
+
const existing = import_node_fs10.existsSync(path) ? import_node_fs10.readFileSync(path, "utf-8") : "";
|
|
5804
5894
|
const entries = parseEnvContent(existing);
|
|
5805
5895
|
entries.set(key, value);
|
|
5806
5896
|
const output2 = Array.from(entries, ([k, v]) => `${k}=${v}`).join(`
|
|
@@ -5808,11 +5898,11 @@ function writeDotEnv(key, value) {
|
|
|
5808
5898
|
`;
|
|
5809
5899
|
const tmpPath = `${path}.tmp.${process.pid}.${import_node_crypto2.randomBytes(4).toString("hex")}`;
|
|
5810
5900
|
try {
|
|
5811
|
-
|
|
5812
|
-
|
|
5901
|
+
import_node_fs10.writeFileSync(tmpPath, output2, { encoding: "utf-8", mode: 384 });
|
|
5902
|
+
import_node_fs10.renameSync(tmpPath, path);
|
|
5813
5903
|
} catch (error) {
|
|
5814
5904
|
try {
|
|
5815
|
-
|
|
5905
|
+
import_node_fs10.rmSync(tmpPath, { force: true });
|
|
5816
5906
|
} catch {}
|
|
5817
5907
|
throw error;
|
|
5818
5908
|
}
|
|
@@ -5823,6 +5913,7 @@ var DEFAULT_ENV_HOSTS = {
|
|
|
5823
5913
|
production: "openclaw-service.yoooclaw.com"
|
|
5824
5914
|
};
|
|
5825
5915
|
var NOTIFICATION_INTELLIGENCE_LIGHT_RULES_PLUGIN_PATH = "/api/plugin/notification-intelligence/light-rules";
|
|
5916
|
+
var NOTIFICATION_INTELLIGENCE_LIGHT_EFFECTS_SEND_PLUGIN_PATH = "/api/plugin/notification-intelligence/light-effects/send";
|
|
5826
5917
|
var NOTIFICATION_INTELLIGENCE_LIGHT_RULES_APP_PATH = "/api/notification-intelligence/light-rules";
|
|
5827
5918
|
var NOTIFICATION_INTELLIGENCE_PLUGIN_BASE_PATH = "/api/plugin/notification-intelligence";
|
|
5828
5919
|
var NOTIFICATION_INTELLIGENCE_APP_BASE_PATH = "/api/notification-intelligence";
|
|
@@ -5870,13 +5961,29 @@ function normalizeNotificationIntelligenceLightRulesPluginUrl(url) {
|
|
|
5870
5961
|
}
|
|
5871
5962
|
return trimmed;
|
|
5872
5963
|
}
|
|
5964
|
+
function normalizeNotificationIntelligenceLightEffectsSendUrl(url) {
|
|
5965
|
+
const trimmed = url.trim().replace(/\/+$/, "");
|
|
5966
|
+
if (!trimmed)
|
|
5967
|
+
return trimmed;
|
|
5968
|
+
if (trimmed.endsWith(NOTIFICATION_INTELLIGENCE_LIGHT_EFFECTS_SEND_PLUGIN_PATH)) {
|
|
5969
|
+
return trimmed;
|
|
5970
|
+
}
|
|
5971
|
+
const normalizedRulesUrl = normalizeNotificationIntelligenceLightRulesPluginUrl(trimmed);
|
|
5972
|
+
if (normalizedRulesUrl.endsWith(NOTIFICATION_INTELLIGENCE_LIGHT_RULES_PLUGIN_PATH)) {
|
|
5973
|
+
const origin = normalizedRulesUrl.slice(0, -NOTIFICATION_INTELLIGENCE_LIGHT_RULES_PLUGIN_PATH.length);
|
|
5974
|
+
return `${origin}${NOTIFICATION_INTELLIGENCE_LIGHT_EFFECTS_SEND_PLUGIN_PATH}`;
|
|
5975
|
+
}
|
|
5976
|
+
return `${trimmed}/light-effects/send`;
|
|
5977
|
+
}
|
|
5873
5978
|
function buildEnvUrls(host) {
|
|
5874
5979
|
const https = `https://${host}`;
|
|
5875
5980
|
const wss = `wss://${host}`;
|
|
5876
5981
|
const lightRulesUrl = normalizeNotificationIntelligenceLightRulesPluginUrl(process.env.NOTIFICATION_INTELLIGENCE_LIGHT_RULES_URL || `${https}${NOTIFICATION_INTELLIGENCE_LIGHT_RULES_PLUGIN_PATH}`);
|
|
5982
|
+
const lightEffectsSendUrl = normalizeNotificationIntelligenceLightEffectsSendUrl(process.env.NOTIFICATION_INTELLIGENCE_LIGHT_EFFECTS_SEND_URL || lightRulesUrl);
|
|
5877
5983
|
return {
|
|
5878
5984
|
lightApiUrl: `${https}/api/message/tob/sendMessage`,
|
|
5879
5985
|
notificationIntelligenceLightRulesPluginUrl: lightRulesUrl,
|
|
5986
|
+
notificationIntelligenceLightEffectsSendUrl: lightEffectsSendUrl,
|
|
5880
5987
|
relayTunnelUrl: `${wss}/message/messages/ws/plugin`,
|
|
5881
5988
|
appNameMapUrl: `${https}/api/application-config/app-package/config-all`,
|
|
5882
5989
|
modelProxyLongRecordingSubmitTaskUrl: `${https}/api/model-proxy/long-recording/submit-task`,
|
|
@@ -5892,9 +5999,9 @@ var VALID_ENVS = new Set([
|
|
|
5892
5999
|
]);
|
|
5893
6000
|
function readDotEnv() {
|
|
5894
6001
|
const path = resolveStateFile(".env");
|
|
5895
|
-
if (!
|
|
6002
|
+
if (!import_node_fs10.existsSync(path))
|
|
5896
6003
|
return {};
|
|
5897
|
-
return Object.fromEntries(parseEnvContent(
|
|
6004
|
+
return Object.fromEntries(parseEnvContent(import_node_fs10.readFileSync(path, "utf-8")));
|
|
5898
6005
|
}
|
|
5899
6006
|
function readPersistedEnvName() {
|
|
5900
6007
|
const fromDotEnv = readDotEnv()["PHONE_NOTIFICATIONS_ENV"]?.trim();
|
|
@@ -6251,6 +6358,10 @@ function readTrimmedString(value) {
|
|
|
6251
6358
|
const trimmed = value.trim();
|
|
6252
6359
|
return trimmed || undefined;
|
|
6253
6360
|
}
|
|
6361
|
+
var NOTIFICATION_RULE_SIGNAL = /(?:通知|消息|推送|来电|短信|邮件|微信|飞书|钉钉|notification|message|push|incoming\s+call|sms|e-?mail)/i;
|
|
6362
|
+
function isNotificationTriggeredRuleText(ruleText) {
|
|
6363
|
+
return NOTIFICATION_RULE_SIGNAL.test(ruleText);
|
|
6364
|
+
}
|
|
6254
6365
|
function hasAnyPlainUpdateField(params) {
|
|
6255
6366
|
return [
|
|
6256
6367
|
"title",
|
|
@@ -6366,6 +6477,9 @@ function registerLightRulesTools(api, service, logger) {
|
|
|
6366
6477
|
const ruleText = readTrimmedString(params.ruleText);
|
|
6367
6478
|
if (!ruleText)
|
|
6368
6479
|
return err("INVALID_PARAMS", "ruleText is required");
|
|
6480
|
+
if (!isNotificationTriggeredRuleText(ruleText)) {
|
|
6481
|
+
return err("INVALID_RULE_SCOPE", "持久灯效规则必须包含通知、消息、来电等触发条件;一次性亮灯请调用 light_control");
|
|
6482
|
+
}
|
|
6369
6483
|
try {
|
|
6370
6484
|
const result = await service.create({ ruleText });
|
|
6371
6485
|
logger.info(`${LIGHT_RULE_TOOL_NAMES.create} tool: created ${result.id}/${result.name}`);
|
|
@@ -6483,6 +6597,409 @@ function registerLightRulesTools(api, service, logger) {
|
|
|
6483
6597
|
});
|
|
6484
6598
|
}
|
|
6485
6599
|
|
|
6600
|
+
// src/light/sender.ts
|
|
6601
|
+
var import_node_crypto3 = require("node:crypto");
|
|
6602
|
+
async function sendLightEffect(apiKey, segments, logger, repeatInput, reason, title, options) {
|
|
6603
|
+
const resolvedTitle = resolveLightTitle(title, reason, segments);
|
|
6604
|
+
let repeatTimes;
|
|
6605
|
+
try {
|
|
6606
|
+
repeatTimes = normalizeRepeatTimes(repeatInput);
|
|
6607
|
+
} catch (error) {
|
|
6608
|
+
return { ok: false, error: error?.message ?? String(error) };
|
|
6609
|
+
}
|
|
6610
|
+
const intelligence = await sendViaNotificationIntelligence(apiKey, segments, repeatTimes, resolvedTitle, reason, options?.bizUniqueId, logger);
|
|
6611
|
+
if (!intelligence.fallback) {
|
|
6612
|
+
return intelligence.result;
|
|
6613
|
+
}
|
|
6614
|
+
logger?.warn(`Light sender: notification-intelligence entry unavailable (${intelligence.fallbackReason}), falling back to legacy message-service`);
|
|
6615
|
+
return sendViaLegacyMessageService(apiKey, segments, logger, repeatInput, reason, resolvedTitle);
|
|
6616
|
+
}
|
|
6617
|
+
async function sendViaNotificationIntelligence(apiKey, segments, repeatTimes, title, reason, bizUniqueId, logger) {
|
|
6618
|
+
const url = getEnvUrls().notificationIntelligenceLightEffectsSendUrl;
|
|
6619
|
+
const requestBody = {
|
|
6620
|
+
title,
|
|
6621
|
+
repeat_times: repeatTimes,
|
|
6622
|
+
segments
|
|
6623
|
+
};
|
|
6624
|
+
if (reason?.trim())
|
|
6625
|
+
requestBody.reason = reason;
|
|
6626
|
+
const trimmedBizUniqueId = bizUniqueId?.trim();
|
|
6627
|
+
if (trimmedBizUniqueId)
|
|
6628
|
+
requestBody.bizUniqueId = trimmedBizUniqueId;
|
|
6629
|
+
logger?.info(`Light sender: POST ${url}, bizUniqueId=${trimmedBizUniqueId ?? "(server-generated)"}, ` + `title=${title}, repeat_times=${repeatTimes}, segments_count=${segments.length}`);
|
|
6630
|
+
let res;
|
|
6631
|
+
try {
|
|
6632
|
+
res = await fetch(url, {
|
|
6633
|
+
method: "POST",
|
|
6634
|
+
headers: {
|
|
6635
|
+
"Content-Type": "application/json",
|
|
6636
|
+
"X-Api-Key-Id": stripBearer(apiKey)
|
|
6637
|
+
},
|
|
6638
|
+
body: JSON.stringify(requestBody)
|
|
6639
|
+
});
|
|
6640
|
+
} catch (error) {
|
|
6641
|
+
return {
|
|
6642
|
+
fallback: true,
|
|
6643
|
+
fallbackReason: `network error: ${error?.message ?? String(error)}`,
|
|
6644
|
+
result: { ok: false, error: error?.message ?? String(error) }
|
|
6645
|
+
};
|
|
6646
|
+
}
|
|
6647
|
+
const resBody = await res.text();
|
|
6648
|
+
if (!res.ok) {
|
|
6649
|
+
if (res.status === 404 || res.status === 405 || res.status === 401 && /jwt is missing/i.test(resBody)) {
|
|
6650
|
+
return {
|
|
6651
|
+
fallback: true,
|
|
6652
|
+
fallbackReason: `HTTP ${res.status}`,
|
|
6653
|
+
result: { ok: false, status: res.status, error: resBody }
|
|
6654
|
+
};
|
|
6655
|
+
}
|
|
6656
|
+
logger?.warn(`Light sender: FAILED ${res.status}, url=${url}, resBody=${resBody.substring(0, 500)}`);
|
|
6657
|
+
return {
|
|
6658
|
+
fallback: false,
|
|
6659
|
+
result: {
|
|
6660
|
+
ok: false,
|
|
6661
|
+
status: res.status,
|
|
6662
|
+
error: resBody,
|
|
6663
|
+
via: "notification-intelligence"
|
|
6664
|
+
}
|
|
6665
|
+
};
|
|
6666
|
+
}
|
|
6667
|
+
let parsed = null;
|
|
6668
|
+
try {
|
|
6669
|
+
parsed = resBody.trim() ? JSON.parse(resBody) : null;
|
|
6670
|
+
} catch {}
|
|
6671
|
+
if (!parsed || typeof parsed !== "object") {
|
|
6672
|
+
logger?.warn(`Light sender: invalid JSON from ${url}, resBody=${resBody.substring(0, 200)}`);
|
|
6673
|
+
return {
|
|
6674
|
+
fallback: false,
|
|
6675
|
+
result: {
|
|
6676
|
+
ok: false,
|
|
6677
|
+
error: "invalid JSON response",
|
|
6678
|
+
via: "notification-intelligence"
|
|
6679
|
+
}
|
|
6680
|
+
};
|
|
6681
|
+
}
|
|
6682
|
+
const data = parsed.data ?? null;
|
|
6683
|
+
if (parsed.code && parsed.code !== "000000") {
|
|
6684
|
+
const message = parsed.msg ?? parsed.message ?? `remote code ${parsed.code}`;
|
|
6685
|
+
logger?.warn(`Light sender: FAILED code=${parsed.code}, msg=${message}`);
|
|
6686
|
+
return {
|
|
6687
|
+
fallback: false,
|
|
6688
|
+
result: {
|
|
6689
|
+
ok: false,
|
|
6690
|
+
error: String(message),
|
|
6691
|
+
response: parsed,
|
|
6692
|
+
via: "notification-intelligence"
|
|
6693
|
+
}
|
|
6694
|
+
};
|
|
6695
|
+
}
|
|
6696
|
+
if (data?.success === false) {
|
|
6697
|
+
const message = data.message ?? "remote business request failed";
|
|
6698
|
+
logger?.warn(`Light sender: FAILED business, message=${message}`);
|
|
6699
|
+
return {
|
|
6700
|
+
fallback: false,
|
|
6701
|
+
result: {
|
|
6702
|
+
ok: false,
|
|
6703
|
+
error: String(message),
|
|
6704
|
+
response: parsed,
|
|
6705
|
+
via: "notification-intelligence"
|
|
6706
|
+
}
|
|
6707
|
+
};
|
|
6708
|
+
}
|
|
6709
|
+
const effectiveBizUniqueId = typeof data?.bizUniqueId === "string" && data.bizUniqueId.trim() || trimmedBizUniqueId;
|
|
6710
|
+
logger?.info(`Light sender: OK bizUniqueId=${effectiveBizUniqueId ?? "-"}, resBody=${resBody.substring(0, 200)}`);
|
|
6711
|
+
return {
|
|
6712
|
+
fallback: false,
|
|
6713
|
+
result: {
|
|
6714
|
+
ok: true,
|
|
6715
|
+
bizUniqueId: effectiveBizUniqueId || undefined,
|
|
6716
|
+
response: parsed,
|
|
6717
|
+
via: "notification-intelligence"
|
|
6718
|
+
}
|
|
6719
|
+
};
|
|
6720
|
+
}
|
|
6721
|
+
async function sendViaLegacyMessageService(apiKey, segments, logger, repeatInput, reason, resolvedTitle) {
|
|
6722
|
+
const apiUrl = getEnvUrls().lightApiUrl;
|
|
6723
|
+
const appKey = "phone-notifications";
|
|
6724
|
+
const templateId = "1990771146010017800";
|
|
6725
|
+
logger?.info(`Light sender: apiUrl=${apiUrl ?? "UNSET"}, appKey=${appKey ? appKey.substring(0, 8) + "…" : "UNSET"}, templateId=${templateId ?? "UNSET"}, apiKey=${apiKey ? apiKey.substring(0, 20) + "…" : "EMPTY"}, title=${resolvedTitle}, reason=${reason ?? ""}, segments=${JSON.stringify(segments)}`);
|
|
6726
|
+
if (!apiUrl || !appKey || !templateId) {
|
|
6727
|
+
return {
|
|
6728
|
+
ok: false,
|
|
6729
|
+
error: "灯效 API 未配置,请确认构建时已封装 OPENCLAW_HOST_* / LIGHT_APP_KEY / LIGHT_TEMPLATE_ID"
|
|
6730
|
+
};
|
|
6731
|
+
}
|
|
6732
|
+
let bizContent;
|
|
6733
|
+
try {
|
|
6734
|
+
bizContent = buildLightEffectApnsBody(segments, repeatInput, reason);
|
|
6735
|
+
} catch (error) {
|
|
6736
|
+
return { ok: false, error: error?.message ?? String(error) };
|
|
6737
|
+
}
|
|
6738
|
+
const bizUniqueId = import_node_crypto3.randomUUID();
|
|
6739
|
+
const requestBody = {
|
|
6740
|
+
appKey,
|
|
6741
|
+
bizMap: { noticeType: "APP_NOTIFICATION_IMPORTANT", title: resolvedTitle, reason },
|
|
6742
|
+
bizUniqueId,
|
|
6743
|
+
paramsMap: { bizContent },
|
|
6744
|
+
pushType: "SPECIFY_PUSH",
|
|
6745
|
+
templateId
|
|
6746
|
+
};
|
|
6747
|
+
logger?.info(`Light sender: POST ${apiUrl}, bizUniqueId=${bizUniqueId}, body=${JSON.stringify(requestBody).substring(0, 500)}`);
|
|
6748
|
+
const res = await fetch(apiUrl, {
|
|
6749
|
+
method: "POST",
|
|
6750
|
+
headers: {
|
|
6751
|
+
"Content-Type": "application/json",
|
|
6752
|
+
"X-Api-Key-Id": stripBearer(apiKey)
|
|
6753
|
+
},
|
|
6754
|
+
body: JSON.stringify(requestBody)
|
|
6755
|
+
});
|
|
6756
|
+
const resBody = await res.text();
|
|
6757
|
+
if (!res.ok) {
|
|
6758
|
+
logger?.warn(`Light sender: FAILED ${res.status}, url=${apiUrl}, resBody=${resBody.substring(0, 500)}`);
|
|
6759
|
+
return { ok: false, status: res.status, error: resBody, via: "message-service" };
|
|
6760
|
+
}
|
|
6761
|
+
logger?.info(`Light sender: OK bizUniqueId=${bizUniqueId}, resBody=${resBody.substring(0, 200)}`);
|
|
6762
|
+
return {
|
|
6763
|
+
ok: true,
|
|
6764
|
+
bizUniqueId,
|
|
6765
|
+
response: JSON.parse(resBody),
|
|
6766
|
+
via: "message-service"
|
|
6767
|
+
};
|
|
6768
|
+
}
|
|
6769
|
+
function stripBearer(apiKey) {
|
|
6770
|
+
return apiKey.startsWith("Bearer ") ? apiKey.slice("Bearer ".length) : apiKey;
|
|
6771
|
+
}
|
|
6772
|
+
function resolveLightTitle(title, reason, segments) {
|
|
6773
|
+
const trimmedTitle = title?.trim();
|
|
6774
|
+
if (trimmedTitle)
|
|
6775
|
+
return trimmedTitle;
|
|
6776
|
+
const trimmedReason = reason?.trim();
|
|
6777
|
+
if (trimmedReason)
|
|
6778
|
+
return trimmedReason;
|
|
6779
|
+
const modeDesc = segments.map((segment) => segment.mode).join("+");
|
|
6780
|
+
return `Effect: ${modeDesc || "custom"}`;
|
|
6781
|
+
}
|
|
6782
|
+
|
|
6783
|
+
// src/plugin/light-control.ts
|
|
6784
|
+
var lightControlParameters = {
|
|
6785
|
+
type: "object",
|
|
6786
|
+
required: ["segments", "reason"],
|
|
6787
|
+
additionalProperties: false,
|
|
6788
|
+
properties: {
|
|
6789
|
+
segments: {
|
|
6790
|
+
type: "array",
|
|
6791
|
+
description: "灯效段序列,1–12 段,按顺序播放",
|
|
6792
|
+
minItems: 1,
|
|
6793
|
+
maxItems: MAX_LIGHT_SEGMENTS,
|
|
6794
|
+
items: {
|
|
6795
|
+
type: "object",
|
|
6796
|
+
required: ["mode", "duration_s"],
|
|
6797
|
+
additionalProperties: false,
|
|
6798
|
+
properties: {
|
|
6799
|
+
mode: {
|
|
6800
|
+
type: "string",
|
|
6801
|
+
enum: ["wave", "breath", "strobe", "steady", "color_flow", "pixel_frame"],
|
|
6802
|
+
description: "灯效模式:wave 波浪(单色跑马,前景色+可选底色)/ breath 呼吸 / strobe 频闪 / steady 常亮 / " + "color_flow 流光(调色板沿圆周流动,1~2 个颜色锚点插值;**不是彩虹**,无法生成 7 色彩虹波浪)/ " + "pixel_frame 逐组像素帧。" + '易错对照:用户说"红色/蓝色波浪"等单一颜色波浪 → 必须用 wave,不要用 color_flow;' + '用户说"彩虹/七彩/rainbow" → 固件不支持真正的 7 色彩虹,需告知限制并给出近似方案(双锚点 color_flow 或多段 wave 轮播),' + "禁止静默用 color_flow 单锚点冒充彩虹。"
|
|
6803
|
+
},
|
|
6804
|
+
duration_s: {
|
|
6805
|
+
type: "number",
|
|
6806
|
+
minimum: 0,
|
|
6807
|
+
description: "本段持续时长(秒),0 表示无限时长。推荐离散值:0.5/1/2/3/5/6/8/16/24/32/48,建议单段 ≤8s 省电"
|
|
6808
|
+
},
|
|
6809
|
+
brightness: {
|
|
6810
|
+
type: "number",
|
|
6811
|
+
minimum: 0,
|
|
6812
|
+
maximum: 255,
|
|
6813
|
+
description: "前景亮度 0–255。非 pixel_frame 模式必填;brightness=0 仅 steady 模式有效(显式灭灯)"
|
|
6814
|
+
},
|
|
6815
|
+
color: {
|
|
6816
|
+
type: "object",
|
|
6817
|
+
required: ["r", "g", "b"],
|
|
6818
|
+
additionalProperties: false,
|
|
6819
|
+
properties: {
|
|
6820
|
+
r: { type: "number", minimum: 0, maximum: 255 },
|
|
6821
|
+
g: { type: "number", minimum: 0, maximum: 255 },
|
|
6822
|
+
b: { type: "number", minimum: 0, maximum: 255 }
|
|
6823
|
+
},
|
|
6824
|
+
description: "前景色 RGB。非 pixel_frame 模式必填;color_flow 下作为第一颜色锚点,至少 color/background 其一非零"
|
|
6825
|
+
},
|
|
6826
|
+
interval_ms: {
|
|
6827
|
+
type: "number",
|
|
6828
|
+
minimum: 0,
|
|
6829
|
+
description: "步进间隔(ms),wave/strobe/color_flow 有效,默认 200。量化档位:50/100/200/300/500/600/800/1600/2400/3200/4800"
|
|
6830
|
+
},
|
|
6831
|
+
direction: {
|
|
6832
|
+
type: "string",
|
|
6833
|
+
enum: ["ltr", "rtl"],
|
|
6834
|
+
description: "波浪/流光方向,wave/color_flow 有效,默认 ltr"
|
|
6835
|
+
},
|
|
6836
|
+
window: {
|
|
6837
|
+
type: "number",
|
|
6838
|
+
enum: [1, 2, 3],
|
|
6839
|
+
description: "wave:窗口灯数,默认 2;color_flow:调色板平移步幅(速度)"
|
|
6840
|
+
},
|
|
6841
|
+
breath_timing: {
|
|
6842
|
+
type: "object",
|
|
6843
|
+
additionalProperties: false,
|
|
6844
|
+
properties: {
|
|
6845
|
+
rise_ms: {
|
|
6846
|
+
type: "number",
|
|
6847
|
+
minimum: 0,
|
|
6848
|
+
description: "上升时间(ms),量化到 1040/1560/2080/2600/3100/4160"
|
|
6849
|
+
},
|
|
6850
|
+
hold_ms: {
|
|
6851
|
+
type: "number",
|
|
6852
|
+
minimum: 0,
|
|
6853
|
+
description: "保持最高亮度时间(ms),支持 0ms"
|
|
6854
|
+
},
|
|
6855
|
+
fall_ms: {
|
|
6856
|
+
type: "number",
|
|
6857
|
+
minimum: 0,
|
|
6858
|
+
description: "下降时间(ms),量化到 1040/1560/2080/2600/3100/4160"
|
|
6859
|
+
},
|
|
6860
|
+
off_ms: {
|
|
6861
|
+
type: "number",
|
|
6862
|
+
minimum: 0,
|
|
6863
|
+
description: "熄灭时间(ms),支持 0ms"
|
|
6864
|
+
}
|
|
6865
|
+
},
|
|
6866
|
+
description: "呼吸四段时间,仅 breath 有效;rise/fall 量化档位:1040/1560/2080/2600/3100/4160,hold/off 额外支持 0ms"
|
|
6867
|
+
},
|
|
6868
|
+
background: {
|
|
6869
|
+
type: "object",
|
|
6870
|
+
required: ["r", "g", "b", "brightness"],
|
|
6871
|
+
additionalProperties: false,
|
|
6872
|
+
properties: {
|
|
6873
|
+
r: { type: "number", minimum: 0, maximum: 255 },
|
|
6874
|
+
g: { type: "number", minimum: 0, maximum: 255 },
|
|
6875
|
+
b: { type: "number", minimum: 0, maximum: 255 },
|
|
6876
|
+
brightness: {
|
|
6877
|
+
type: "number",
|
|
6878
|
+
minimum: 0,
|
|
6879
|
+
maximum: 255,
|
|
6880
|
+
description: "底色亮度,量化档位:0/32/64/96/128/192/255"
|
|
6881
|
+
}
|
|
6882
|
+
},
|
|
6883
|
+
description: "底色 RGB + 亮度,wave 有效为底色层;color_flow 有效为第二颜色锚点(与前景色插值生成多色调色板)"
|
|
6884
|
+
},
|
|
6885
|
+
pixels: {
|
|
6886
|
+
type: "array",
|
|
6887
|
+
minItems: 1,
|
|
6888
|
+
maxItems: 7,
|
|
6889
|
+
items: {
|
|
6890
|
+
type: "object",
|
|
6891
|
+
required: ["index", "color", "brightness"],
|
|
6892
|
+
additionalProperties: false,
|
|
6893
|
+
properties: {
|
|
6894
|
+
index: {
|
|
6895
|
+
type: "number",
|
|
6896
|
+
minimum: 0,
|
|
6897
|
+
maximum: 6,
|
|
6898
|
+
description: "LED 组索引,0–6"
|
|
6899
|
+
},
|
|
6900
|
+
color: {
|
|
6901
|
+
type: "object",
|
|
6902
|
+
required: ["r", "g", "b"],
|
|
6903
|
+
additionalProperties: false,
|
|
6904
|
+
properties: {
|
|
6905
|
+
r: { type: "number", minimum: 0, maximum: 255 },
|
|
6906
|
+
g: { type: "number", minimum: 0, maximum: 255 },
|
|
6907
|
+
b: { type: "number", minimum: 0, maximum: 255 }
|
|
6908
|
+
},
|
|
6909
|
+
description: "该像素的 RGB"
|
|
6910
|
+
},
|
|
6911
|
+
brightness: {
|
|
6912
|
+
type: "number",
|
|
6913
|
+
minimum: 0,
|
|
6914
|
+
maximum: 255,
|
|
6915
|
+
description: "该像素亮度 0–255;0 会编码为像素熄灭"
|
|
6916
|
+
}
|
|
6917
|
+
}
|
|
6918
|
+
},
|
|
6919
|
+
description: "仅 pixel_frame 有效:播放前先清 7 组,再按此列表写入 (Idx,R,G,B,Br)"
|
|
6920
|
+
}
|
|
6921
|
+
}
|
|
6922
|
+
}
|
|
6923
|
+
},
|
|
6924
|
+
reason: {
|
|
6925
|
+
type: "string",
|
|
6926
|
+
description: "结合对话上下文,简要说明为什么要亮灯(例如:用户心情低落想要温暖氛围、庆祝生日、提醒喝水等)。" + "这个字段帮助记录每次亮灯的意图和动机。"
|
|
6927
|
+
},
|
|
6928
|
+
title: {
|
|
6929
|
+
type: "string",
|
|
6930
|
+
description: "对本次亮灯的简短标题/摘要(例如:温暖氛围、生日庆祝、喝水提醒)。不填时默认回退到 reason。"
|
|
6931
|
+
},
|
|
6932
|
+
repeat: {
|
|
6933
|
+
type: "boolean",
|
|
6934
|
+
description: "兼容旧参数。true = 无限循环,false/不填 = 一轮;若同时提供 repeat_times,则以后者为准"
|
|
6935
|
+
},
|
|
6936
|
+
repeat_times: {
|
|
6937
|
+
type: "number",
|
|
6938
|
+
minimum: 0,
|
|
6939
|
+
description: "整条组合重复次数:0 = 无限循环,1 = 播放一轮。当前 ANCS 路径不支持 N>=2"
|
|
6940
|
+
}
|
|
6941
|
+
}
|
|
6942
|
+
};
|
|
6943
|
+
function registerLightControlTool(api, logger) {
|
|
6944
|
+
api.registerTool({
|
|
6945
|
+
name: "light_control",
|
|
6946
|
+
label: "Light Control",
|
|
6947
|
+
description: "立即控制硬件灯效(一次性执行,不会创建或保存通知触发规则),支持 1–12 段顺序灯效,每段独立参数(6 种模式:波浪/呼吸/频闪/常亮/流光/逐组像素帧)。" + "Tool 内部自动量化为嵌入式协议定义的离散档位,并按模式精简为变长 ANCS 控制序列,通过 HTTP 接口下发灯效指令。" + '当用户说"开灯"、"换个灯效"、"把灯调成红色"、"营造氛围"、"测试/预览灯效"等与当前即时灯光控制相关的表达时调用。' + `如果用户说"收到某类通知/消息时亮灯/闪灯/变灯效",那是持久灯效规则,改用 ${LIGHT_RULE_TOOL_NAMES.create} 或 ${LIGHT_RULE_TOOL_NAMES.update}。`,
|
|
6948
|
+
parameters: lightControlParameters,
|
|
6949
|
+
async execute(toolCallId, params) {
|
|
6950
|
+
let apiKey;
|
|
6951
|
+
try {
|
|
6952
|
+
apiKey = requireApiKey();
|
|
6953
|
+
} catch (e) {
|
|
6954
|
+
return {
|
|
6955
|
+
content: [{ type: "text", text: e.message }],
|
|
6956
|
+
details: { ok: false, error: { code: "AUTH_REQUIRED", message: e.message } }
|
|
6957
|
+
};
|
|
6958
|
+
}
|
|
6959
|
+
const { segments, title, reason, repeat, repeat_times } = params;
|
|
6960
|
+
const validation = validateSegments(segments);
|
|
6961
|
+
if (!validation.valid) {
|
|
6962
|
+
return {
|
|
6963
|
+
content: [{ type: "text", text: JSON.stringify(validation.errors) }],
|
|
6964
|
+
details: {
|
|
6965
|
+
ok: false,
|
|
6966
|
+
error: { code: "VALIDATION_FAILED", details: validation.errors }
|
|
6967
|
+
}
|
|
6968
|
+
};
|
|
6969
|
+
}
|
|
6970
|
+
let repeatTimes;
|
|
6971
|
+
try {
|
|
6972
|
+
repeatTimes = normalizeRepeatTimes({ repeat, repeat_times });
|
|
6973
|
+
assertAncsRepeatTimes(repeatTimes);
|
|
6974
|
+
} catch (error) {
|
|
6975
|
+
return {
|
|
6976
|
+
content: [{ type: "text", text: error?.message ?? String(error) }],
|
|
6977
|
+
details: {
|
|
6978
|
+
ok: false,
|
|
6979
|
+
error: { code: "VALIDATION_FAILED", message: error?.message ?? String(error) }
|
|
6980
|
+
}
|
|
6981
|
+
};
|
|
6982
|
+
}
|
|
6983
|
+
logger.info(`Light control title: ${title ?? ""}, reason: ${reason}`);
|
|
6984
|
+
for (const warning of validation.warnings) {
|
|
6985
|
+
logger.warn(`Light control validation warning [${warning.code}] ${warning.field}: ${warning.message}`);
|
|
6986
|
+
}
|
|
6987
|
+
const bizUniqueId = typeof toolCallId === "string" && toolCallId.trim() ? `plugin-tool-${toolCallId.trim()}` : undefined;
|
|
6988
|
+
const result = await sendLightEffect(apiKey, validation.segments, logger, { repeat_times: repeatTimes }, reason, title, { bizUniqueId });
|
|
6989
|
+
if (!result.ok) {
|
|
6990
|
+
logger.warn(`Light control HTTP request failed: ${result.status} ${result.error}`);
|
|
6991
|
+
} else {
|
|
6992
|
+
logger.info(`Light control sent, bizUniqueId=${result.bizUniqueId}`);
|
|
6993
|
+
}
|
|
6994
|
+
const details = validation.warnings.length > 0 ? { ...result, warnings: validation.warnings } : result;
|
|
6995
|
+
return {
|
|
6996
|
+
content: [{ type: "text", text: JSON.stringify(details) }],
|
|
6997
|
+
details
|
|
6998
|
+
};
|
|
6999
|
+
}
|
|
7000
|
+
});
|
|
7001
|
+
}
|
|
7002
|
+
|
|
6486
7003
|
// src/light-rules/registry.ts
|
|
6487
7004
|
class LightRuleRegistry {
|
|
6488
7005
|
ctx;
|
|
@@ -6557,7 +7074,7 @@ class LightRuleRegistry {
|
|
|
6557
7074
|
}
|
|
6558
7075
|
|
|
6559
7076
|
// src/index.ts
|
|
6560
|
-
var
|
|
7077
|
+
var import_node_path33 = require("node:path");
|
|
6561
7078
|
|
|
6562
7079
|
// src/update/channel.ts
|
|
6563
7080
|
var PRODUCTION_UPDATE_BASE_URL = "https://artifact.yoooclaw.com/plugin";
|
|
@@ -6585,8 +7102,8 @@ function resolveUpdateBaseUrl(params) {
|
|
|
6585
7102
|
}
|
|
6586
7103
|
|
|
6587
7104
|
// src/update/index.ts
|
|
6588
|
-
var
|
|
6589
|
-
var
|
|
7105
|
+
var import_node_fs13 = require("node:fs");
|
|
7106
|
+
var import_node_path13 = require("node:path");
|
|
6590
7107
|
|
|
6591
7108
|
// src/plugin/shared.ts
|
|
6592
7109
|
function readBody(req) {
|
|
@@ -6782,9 +7299,9 @@ class UpdateChecker {
|
|
|
6782
7299
|
}
|
|
6783
7300
|
|
|
6784
7301
|
// src/update/executor.ts
|
|
6785
|
-
var
|
|
7302
|
+
var import_node_fs11 = require("node:fs");
|
|
6786
7303
|
var import_node_child_process = require("node:child_process");
|
|
6787
|
-
var
|
|
7304
|
+
var import_node_path11 = require("node:path");
|
|
6788
7305
|
var import_node_os = require("node:os");
|
|
6789
7306
|
var VERSION_PATTERN = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
|
|
6790
7307
|
var UPDATE_DIR_NAME = ".phone-notifications-update";
|
|
@@ -6800,27 +7317,27 @@ function normalizeBaseUrlAndDeps(baseUrlOrDeps, deps) {
|
|
|
6800
7317
|
};
|
|
6801
7318
|
}
|
|
6802
7319
|
function updateRootForTarget(targetDir) {
|
|
6803
|
-
return
|
|
7320
|
+
return import_node_path11.join(import_node_path11.dirname(targetDir), UPDATE_DIR_NAME);
|
|
6804
7321
|
}
|
|
6805
7322
|
function lastUpdateStatusPath(targetDir) {
|
|
6806
|
-
return
|
|
7323
|
+
return import_node_path11.join(updateRootForTarget(targetDir), LAST_UPDATE_FILE);
|
|
6807
7324
|
}
|
|
6808
7325
|
function writeJsonAtomic(path, value) {
|
|
6809
|
-
|
|
7326
|
+
import_node_fs11.mkdirSync(import_node_path11.dirname(path), { recursive: true });
|
|
6810
7327
|
const tmpPath = `${path}.tmp.${process.pid}.${Date.now()}`;
|
|
6811
|
-
|
|
7328
|
+
import_node_fs11.writeFileSync(tmpPath, `${JSON.stringify(value, null, 2)}
|
|
6812
7329
|
`);
|
|
6813
|
-
|
|
7330
|
+
import_node_fs11.renameSync(tmpPath, path);
|
|
6814
7331
|
}
|
|
6815
7332
|
function writeLastUpdateStatus(status) {
|
|
6816
7333
|
writeJsonAtomic(lastUpdateStatusPath(status.targetDir), status);
|
|
6817
7334
|
}
|
|
6818
7335
|
function readLastUpdateStatus(targetDir) {
|
|
6819
7336
|
const path = lastUpdateStatusPath(targetDir);
|
|
6820
|
-
if (!
|
|
7337
|
+
if (!import_node_fs11.existsSync(path))
|
|
6821
7338
|
return;
|
|
6822
7339
|
try {
|
|
6823
|
-
return JSON.parse(
|
|
7340
|
+
return JSON.parse(import_node_fs11.readFileSync(path, "utf8"));
|
|
6824
7341
|
} catch {
|
|
6825
7342
|
return;
|
|
6826
7343
|
}
|
|
@@ -6878,17 +7395,17 @@ function isCrossDeviceError(err2) {
|
|
|
6878
7395
|
}
|
|
6879
7396
|
function movePathSync(source, target) {
|
|
6880
7397
|
try {
|
|
6881
|
-
|
|
7398
|
+
import_node_fs11.renameSync(source, target);
|
|
6882
7399
|
} catch (err2) {
|
|
6883
7400
|
if (!isCrossDeviceError(err2))
|
|
6884
7401
|
throw err2;
|
|
6885
|
-
|
|
7402
|
+
import_node_fs11.cpSync(source, target, {
|
|
6886
7403
|
errorOnExist: false,
|
|
6887
7404
|
force: true,
|
|
6888
7405
|
recursive: true,
|
|
6889
7406
|
verbatimSymlinks: true
|
|
6890
7407
|
});
|
|
6891
|
-
|
|
7408
|
+
import_node_fs11.rmSync(source, { recursive: true, force: true });
|
|
6892
7409
|
}
|
|
6893
7410
|
}
|
|
6894
7411
|
function buildLastUpdateNotice(status) {
|
|
@@ -6951,23 +7468,11 @@ function markLastUpdateRestartStatus(targetDir, restartStatus, fields = {}) {
|
|
|
6951
7468
|
updatedAt: new Date().toISOString()
|
|
6952
7469
|
});
|
|
6953
7470
|
}
|
|
6954
|
-
function markLastUpdateReconciled(targetDir) {
|
|
6955
|
-
const status = readLastUpdateStatus(targetDir);
|
|
6956
|
-
if (!status || status.status !== "succeeded")
|
|
6957
|
-
return;
|
|
6958
|
-
if (status.reconciledAt)
|
|
6959
|
-
return;
|
|
6960
|
-
writeLastUpdateStatus({
|
|
6961
|
-
...status,
|
|
6962
|
-
reconciledAt: new Date().toISOString(),
|
|
6963
|
-
updatedAt: new Date().toISOString()
|
|
6964
|
-
});
|
|
6965
|
-
}
|
|
6966
7471
|
function cleanupPendingDir(status) {
|
|
6967
7472
|
if (!status?.pendingDir)
|
|
6968
7473
|
return;
|
|
6969
7474
|
try {
|
|
6970
|
-
|
|
7475
|
+
import_node_fs11.rmSync(status.pendingDir, { recursive: true, force: true });
|
|
6971
7476
|
} catch {}
|
|
6972
7477
|
}
|
|
6973
7478
|
function applyPreparedUpdate(status, restartStatus = status.restartStatus ?? "pending") {
|
|
@@ -7001,12 +7506,12 @@ function applyPreparedUpdate(status, restartStatus = status.restartStatus ?? "pe
|
|
|
7001
7506
|
};
|
|
7002
7507
|
writeLastUpdateStatus(applyingStatus);
|
|
7003
7508
|
try {
|
|
7004
|
-
if (
|
|
7509
|
+
if (import_node_fs11.existsSync(status.targetDir)) {
|
|
7005
7510
|
movePathSync(status.targetDir, backupDir);
|
|
7006
7511
|
}
|
|
7007
7512
|
movePathSync(status.packageDir, status.targetDir);
|
|
7008
|
-
|
|
7009
|
-
|
|
7513
|
+
import_node_fs11.rmSync(backupDir, { recursive: true, force: true });
|
|
7514
|
+
import_node_fs11.rmSync(status.pendingDir, { recursive: true, force: true });
|
|
7010
7515
|
writeLastUpdateStatus({
|
|
7011
7516
|
...applyingStatus,
|
|
7012
7517
|
status: "succeeded",
|
|
@@ -7028,7 +7533,7 @@ function applyPreparedUpdate(status, restartStatus = status.restartStatus ?? "pe
|
|
|
7028
7533
|
} catch (err2) {
|
|
7029
7534
|
const error = String(err2 instanceof Error ? err2.stack ?? err2.message : err2);
|
|
7030
7535
|
try {
|
|
7031
|
-
if (!
|
|
7536
|
+
if (!import_node_fs11.existsSync(status.targetDir) && import_node_fs11.existsSync(backupDir)) {
|
|
7032
7537
|
movePathSync(backupDir, status.targetDir);
|
|
7033
7538
|
}
|
|
7034
7539
|
} catch {}
|
|
@@ -7205,13 +7710,13 @@ async function executeUpdate(version, runCommand, logger, targetDir, baseUrlOrDe
|
|
|
7205
7710
|
const updateId = createUpdateId(now, deps.random?.() ?? Math.random());
|
|
7206
7711
|
const updateRoot = updateRootForTarget(targetDir);
|
|
7207
7712
|
const statusPath = lastUpdateStatusPath(targetDir);
|
|
7208
|
-
const pendingDir =
|
|
7209
|
-
const packageDir =
|
|
7713
|
+
const pendingDir = import_node_path11.join(updateRoot, updateId);
|
|
7714
|
+
const packageDir = import_node_path11.join(pendingDir, "package");
|
|
7210
7715
|
let workDir = "";
|
|
7211
7716
|
try {
|
|
7212
|
-
|
|
7213
|
-
workDir =
|
|
7214
|
-
const tgzPath =
|
|
7717
|
+
import_node_fs11.mkdirSync(updateRoot, { recursive: true });
|
|
7718
|
+
workDir = import_node_fs11.mkdtempSync(import_node_path11.join(updateRoot, ".work-"));
|
|
7719
|
+
const tgzPath = import_node_path11.join(workDir, "plugin.tgz");
|
|
7215
7720
|
const existingStatus = readLastUpdateStatus(targetDir);
|
|
7216
7721
|
if (existingStatus && isActiveUpdate(existingStatus, now)) {
|
|
7217
7722
|
return {
|
|
@@ -7240,9 +7745,9 @@ async function executeUpdate(version, runCommand, logger, targetDir, baseUrlOrDe
|
|
|
7240
7745
|
};
|
|
7241
7746
|
}
|
|
7242
7747
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
7243
|
-
|
|
7748
|
+
import_node_fs11.writeFileSync(tgzPath, buffer);
|
|
7244
7749
|
logger.info(`下载完成 (${buffer.length} bytes)`);
|
|
7245
|
-
|
|
7750
|
+
import_node_fs11.mkdirSync(packageDir, { recursive: true });
|
|
7246
7751
|
const tarResult = await runCommand(["tar", "-xzf", tgzPath, "-C", packageDir, "--strip-components=1"], { timeoutMs: 30000 });
|
|
7247
7752
|
if (tarResult.code !== 0) {
|
|
7248
7753
|
const err2 = tarResult.stderr || tarResult.stdout || "unknown error";
|
|
@@ -7321,7 +7826,7 @@ async function executeUpdate(version, runCommand, logger, targetDir, baseUrlOrDe
|
|
|
7321
7826
|
} finally {
|
|
7322
7827
|
try {
|
|
7323
7828
|
if (workDir)
|
|
7324
|
-
|
|
7829
|
+
import_node_fs11.rmSync(workDir, { force: true, recursive: true });
|
|
7325
7830
|
} catch {}
|
|
7326
7831
|
}
|
|
7327
7832
|
}
|
|
@@ -7397,6 +7902,123 @@ function scheduleGatewayRestart(logger, deps = {}) {
|
|
|
7397
7902
|
};
|
|
7398
7903
|
}
|
|
7399
7904
|
|
|
7905
|
+
// src/update/stale-runtime.ts
|
|
7906
|
+
var import_node_child_process2 = require("node:child_process");
|
|
7907
|
+
var import_node_fs12 = require("node:fs");
|
|
7908
|
+
var import_node_path12 = require("node:path");
|
|
7909
|
+
var ESCALATION_FILE = "restart-escalation.json";
|
|
7910
|
+
var ESCALATION_SPAWN_DELAY_MS = 5000;
|
|
7911
|
+
function readPackageVersion(packageJsonPath) {
|
|
7912
|
+
try {
|
|
7913
|
+
const parsed = JSON.parse(import_node_fs12.readFileSync(packageJsonPath, "utf8"));
|
|
7914
|
+
return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version.trim() : undefined;
|
|
7915
|
+
} catch {
|
|
7916
|
+
return;
|
|
7917
|
+
}
|
|
7918
|
+
}
|
|
7919
|
+
function detectStalePluginRuntime(targetDir, runningVersion = PLUGIN_VERSION) {
|
|
7920
|
+
const diskVersion = readPackageVersion(import_node_path12.join(targetDir, "package.json"));
|
|
7921
|
+
if (!diskVersion || !isNewerVersion(diskVersion, runningVersion)) {
|
|
7922
|
+
return { stale: false, runningVersion, diskVersion };
|
|
7923
|
+
}
|
|
7924
|
+
return { stale: true, runningVersion, diskVersion };
|
|
7925
|
+
}
|
|
7926
|
+
function escalationRecordPath(targetDir) {
|
|
7927
|
+
return import_node_path12.join(updateRootForTarget(targetDir), ESCALATION_FILE);
|
|
7928
|
+
}
|
|
7929
|
+
function readEscalationRecord(targetDir) {
|
|
7930
|
+
try {
|
|
7931
|
+
const parsed = JSON.parse(import_node_fs12.readFileSync(escalationRecordPath(targetDir), "utf8"));
|
|
7932
|
+
return typeof parsed?.version === "string" ? parsed : undefined;
|
|
7933
|
+
} catch {
|
|
7934
|
+
return;
|
|
7935
|
+
}
|
|
7936
|
+
}
|
|
7937
|
+
function writeEscalationRecord(targetDir, record) {
|
|
7938
|
+
const path = escalationRecordPath(targetDir);
|
|
7939
|
+
import_node_fs12.mkdirSync(import_node_path12.dirname(path), { recursive: true });
|
|
7940
|
+
const tmpPath = `${path}.tmp.${process.pid}`;
|
|
7941
|
+
import_node_fs12.writeFileSync(tmpPath, `${JSON.stringify(record, null, 2)}
|
|
7942
|
+
`);
|
|
7943
|
+
import_node_fs12.renameSync(tmpPath, path);
|
|
7944
|
+
}
|
|
7945
|
+
function resolveRestartCommand(deps) {
|
|
7946
|
+
const argv1 = deps.argv1 ?? process.argv[1];
|
|
7947
|
+
const execPath = deps.execPath ?? process.execPath;
|
|
7948
|
+
if (argv1 && (/openclaw/i.test(import_node_path12.basename(argv1)) || /openclaw/i.test(argv1))) {
|
|
7949
|
+
return [execPath, argv1, "gateway", "restart"];
|
|
7950
|
+
}
|
|
7951
|
+
return ["openclaw", "gateway", "restart"];
|
|
7952
|
+
}
|
|
7953
|
+
function maybeEscalateStaleRuntimeRestart(logger, targetDir, deps = {}) {
|
|
7954
|
+
const info = detectStalePluginRuntime(targetDir, deps.runningVersion);
|
|
7955
|
+
if (!info.stale || !info.diskVersion)
|
|
7956
|
+
return { action: "none" };
|
|
7957
|
+
const diskVersion = info.diskVersion;
|
|
7958
|
+
logger.error(`插件更新未生效:磁盘已安装 ${diskVersion},进程仍在运行 ${info.runningVersion}。` + "宿主的进程内重启不会重新加载插件代码,需要完整重启 openclaw 进程。");
|
|
7959
|
+
try {
|
|
7960
|
+
const cfg = deps.loadConfig?.();
|
|
7961
|
+
if (cfg?.commands?.restart === false) {
|
|
7962
|
+
return {
|
|
7963
|
+
action: "manual-required",
|
|
7964
|
+
diskVersion,
|
|
7965
|
+
reason: "commands.restart=false,自动重启被配置禁用"
|
|
7966
|
+
};
|
|
7967
|
+
}
|
|
7968
|
+
} catch (err2) {
|
|
7969
|
+
logger.warn(`陈旧运行时检查读取配置失败: ${String(err2)}`);
|
|
7970
|
+
}
|
|
7971
|
+
const attempted = readEscalationRecord(targetDir);
|
|
7972
|
+
if (attempted?.version === diskVersion) {
|
|
7973
|
+
return {
|
|
7974
|
+
action: "manual-required",
|
|
7975
|
+
diskVersion,
|
|
7976
|
+
reason: `已为 ${diskVersion} 自动触发过一次完整重启但未生效(宿主可能未由 service manager 托管)`
|
|
7977
|
+
};
|
|
7978
|
+
}
|
|
7979
|
+
const command = resolveRestartCommand(deps);
|
|
7980
|
+
const now = deps.now?.() ?? new Date;
|
|
7981
|
+
try {
|
|
7982
|
+
writeEscalationRecord(targetDir, {
|
|
7983
|
+
version: diskVersion,
|
|
7984
|
+
attemptedAt: now.toISOString()
|
|
7985
|
+
});
|
|
7986
|
+
} catch (err2) {
|
|
7987
|
+
return {
|
|
7988
|
+
action: "manual-required",
|
|
7989
|
+
diskVersion,
|
|
7990
|
+
reason: `无法记录自动重启尝试(${String(err2)}),为避免重启循环不再自动触发`
|
|
7991
|
+
};
|
|
7992
|
+
}
|
|
7993
|
+
const spawnFn = deps.spawnFn ?? import_node_child_process2.spawn;
|
|
7994
|
+
const scheduleFn = deps.scheduleFn ?? ((task, delayMs) => setTimeout(task, delayMs));
|
|
7995
|
+
const timer = scheduleFn(() => {
|
|
7996
|
+
try {
|
|
7997
|
+
const child = spawnFn(command[0], command.slice(1), {
|
|
7998
|
+
detached: true,
|
|
7999
|
+
stdio: "ignore"
|
|
8000
|
+
});
|
|
8001
|
+
child.unref?.();
|
|
8002
|
+
logger.warn(`已触发完整 gateway 重启以加载插件 ${diskVersion}: ${command.join(" ")}`);
|
|
8003
|
+
} catch (err2) {
|
|
8004
|
+
logger.error(`自动完整重启失败(${String(err2)}),请手动完整重启 openclaw 进程以加载 ${diskVersion}`);
|
|
8005
|
+
}
|
|
8006
|
+
}, ESCALATION_SPAWN_DELAY_MS);
|
|
8007
|
+
timer.unref?.();
|
|
8008
|
+
logger.warn(`将在 ${ESCALATION_SPAWN_DELAY_MS / 1000}s 后尝试完整重启 gateway(openclaw gateway restart)以加载 ${diskVersion}`);
|
|
8009
|
+
return { action: "restart-scheduled", diskVersion, command };
|
|
8010
|
+
}
|
|
8011
|
+
function buildStaleRuntimePromptNotice(info, escalation) {
|
|
8012
|
+
if (!info.stale || !info.diskVersion)
|
|
8013
|
+
return;
|
|
8014
|
+
const base = `[系统通知] phone-notifications 插件已更新到 ${info.diskVersion} 并安装完成,` + `但当前进程仍在运行旧版本 ${info.runningVersion}:宿主的进程内重启(SIGUSR1 / ` + `gateway.restart)不会重新加载插件代码。`;
|
|
8015
|
+
if (escalation.action === "restart-scheduled") {
|
|
8016
|
+
return base + "系统已自动触发一次完整重启,若几分钟后此提示仍出现,说明自动重启未生效," + "请告知用户需要完整重启 openclaw 进程(例如 systemctl restart openclaw," + "或结束进程后由服务管理器拉起)。";
|
|
8017
|
+
}
|
|
8018
|
+
const reason = escalation.action === "manual-required" ? `(${escalation.reason})` : "";
|
|
8019
|
+
return base + `请告知用户需要完整重启 openclaw 进程才能生效${reason}:` + "例如 systemctl restart openclaw,或结束 openclaw 进程后由服务管理器拉起;" + "仅执行进程内的 gateway 重启无效。";
|
|
8020
|
+
}
|
|
8021
|
+
|
|
7400
8022
|
// src/update/index.ts
|
|
7401
8023
|
|
|
7402
8024
|
var PLUGIN_ID2 = "phone-notifications";
|
|
@@ -7408,32 +8030,7 @@ function shouldSuppressUpdateNotifications() {
|
|
|
7408
8030
|
return process.platform === "win32";
|
|
7409
8031
|
}
|
|
7410
8032
|
function resolveTargetDir(api) {
|
|
7411
|
-
|
|
7412
|
-
const cfg = api.runtime.config?.loadConfig?.();
|
|
7413
|
-
const installPath = cfg?.plugins?.installs?.[PLUGIN_ID2]?.installPath;
|
|
7414
|
-
if (installPath)
|
|
7415
|
-
return installPath;
|
|
7416
|
-
} catch {}
|
|
7417
|
-
return import_node_path11.join(resolvePluginStateDir(api), "extensions", PLUGIN_ID2);
|
|
7418
|
-
}
|
|
7419
|
-
async function updateConfigRecord(api, version, targetDir, tgzUrl) {
|
|
7420
|
-
const configApi = api.runtime.config;
|
|
7421
|
-
if (!configApi)
|
|
7422
|
-
return;
|
|
7423
|
-
const cfg = configApi.loadConfig();
|
|
7424
|
-
if (!cfg.plugins)
|
|
7425
|
-
cfg.plugins = {};
|
|
7426
|
-
if (!cfg.plugins.installs)
|
|
7427
|
-
cfg.plugins.installs = {};
|
|
7428
|
-
cfg.plugins.installs[PLUGIN_ID2] = {
|
|
7429
|
-
...cfg.plugins.installs[PLUGIN_ID2],
|
|
7430
|
-
source: "archive",
|
|
7431
|
-
sourcePath: tgzUrl,
|
|
7432
|
-
installPath: targetDir,
|
|
7433
|
-
version,
|
|
7434
|
-
installedAt: new Date().toISOString()
|
|
7435
|
-
};
|
|
7436
|
-
await configApi.writeConfigFile(cfg);
|
|
8033
|
+
return import_node_path13.join(resolvePluginStateDir(api), "extensions", PLUGIN_ID2);
|
|
7437
8034
|
}
|
|
7438
8035
|
function applyManualRestart(logger, result, targetDir) {
|
|
7439
8036
|
const version = result.version ?? "目标版本";
|
|
@@ -7528,25 +8125,12 @@ function finalizeUpdateResult(api, logger, result, targetDir) {
|
|
|
7528
8125
|
message: `已应用更新到 ${version},正在重启 gateway`
|
|
7529
8126
|
};
|
|
7530
8127
|
}
|
|
7531
|
-
async function reconcileSucceededUpdateRecord(api, logger, targetDir) {
|
|
7532
|
-
const status = readLastUpdateStatus(targetDir);
|
|
7533
|
-
if (status?.status !== "succeeded" || !status.tgzUrl)
|
|
7534
|
-
return;
|
|
7535
|
-
if (status.reconciledAt)
|
|
7536
|
-
return;
|
|
7537
|
-
try {
|
|
7538
|
-
await updateConfigRecord(api, status.version, targetDir, status.tgzUrl);
|
|
7539
|
-
markLastUpdateReconciled(targetDir);
|
|
7540
|
-
} catch (err2) {
|
|
7541
|
-
logger.warn(`更新已应用,但配置记录补写失败: ${String(err2)}`);
|
|
7542
|
-
}
|
|
7543
|
-
}
|
|
7544
8128
|
function resolveModulePackageJsonPath() {
|
|
7545
|
-
return
|
|
8129
|
+
return import_node_path13.join(__dirname, "..", "package.json");
|
|
7546
8130
|
}
|
|
7547
8131
|
function resolveEffectiveCurrentVersion(targetDir, modulePackageJsonPath = resolveModulePackageJsonPath()) {
|
|
7548
8132
|
const installedVersion = pickNewestVersion([
|
|
7549
|
-
readPackageVersionAt(
|
|
8133
|
+
readPackageVersionAt(import_node_path13.join(targetDir, "package.json")),
|
|
7550
8134
|
readPackageVersionAt(modulePackageJsonPath)
|
|
7551
8135
|
]);
|
|
7552
8136
|
if (installedVersion && isNewerVersion(installedVersion, PLUGIN_VERSION)) {
|
|
@@ -7558,9 +8142,39 @@ function resolveEffectiveCurrentVersion(targetDir, modulePackageJsonPath = resol
|
|
|
7558
8142
|
}
|
|
7559
8143
|
return PLUGIN_VERSION;
|
|
7560
8144
|
}
|
|
8145
|
+
function registerPluginUpdateTool(api, logger, baseUrl, onSuccess) {
|
|
8146
|
+
api.registerTool({
|
|
8147
|
+
name: "plugin-update",
|
|
8148
|
+
label: "Plugin Update",
|
|
8149
|
+
description: "将 phone-notifications 插件更新到指定版本。" + "仅在用户明确同意更新后调用。更新完成后会自动尝试重启 gateway;如宿主不支持,则需手动重启。",
|
|
8150
|
+
parameters: {
|
|
8151
|
+
type: "object",
|
|
8152
|
+
required: ["version"],
|
|
8153
|
+
additionalProperties: false,
|
|
8154
|
+
properties: {
|
|
8155
|
+
version: {
|
|
8156
|
+
type: "string",
|
|
8157
|
+
description: "目标版本号,如 1.11.0"
|
|
8158
|
+
}
|
|
8159
|
+
}
|
|
8160
|
+
},
|
|
8161
|
+
async execute(_toolCallId, params) {
|
|
8162
|
+
const { version } = params;
|
|
8163
|
+
const targetDir = resolveTargetDir(api);
|
|
8164
|
+
const result = await executeUpdate(version, api.runtime.system.runCommandWithTimeout, logger, targetDir, baseUrl);
|
|
8165
|
+
const finalResult = finalizeUpdateResult(api, logger, result, targetDir);
|
|
8166
|
+
if (finalResult.success)
|
|
8167
|
+
onSuccess?.();
|
|
8168
|
+
return {
|
|
8169
|
+
content: [{ type: "text", text: finalResult.message }],
|
|
8170
|
+
details: finalResult
|
|
8171
|
+
};
|
|
8172
|
+
}
|
|
8173
|
+
});
|
|
8174
|
+
}
|
|
7561
8175
|
function readPackageVersionAt(packageJsonPath) {
|
|
7562
8176
|
try {
|
|
7563
|
-
const parsed = JSON.parse(
|
|
8177
|
+
const parsed = JSON.parse(import_node_fs13.readFileSync(packageJsonPath, "utf-8"));
|
|
7564
8178
|
const version = typeof parsed.version === "string" ? parsed.version.trim() : "";
|
|
7565
8179
|
return VERSION_PATTERN2.test(version) ? version : undefined;
|
|
7566
8180
|
} catch {
|
|
@@ -7574,11 +8188,15 @@ function registerAutoUpdate(api, logger, config, getBroadcast, rememberBroadcast
|
|
|
7574
8188
|
}
|
|
7575
8189
|
let pendingUpdate = null;
|
|
7576
8190
|
let pendingGatewayUpdate = null;
|
|
8191
|
+
let staleRuntimeInfo = {
|
|
8192
|
+
stale: false,
|
|
8193
|
+
runningVersion: PLUGIN_VERSION
|
|
8194
|
+
};
|
|
8195
|
+
let staleRuntimeEscalation = { action: "none" };
|
|
7577
8196
|
const intervalMs = (config.checkIntervalHours ?? 4) * 3600000;
|
|
7578
8197
|
const channel = config.channel ?? "latest";
|
|
7579
8198
|
const baseUrl = config.baseUrl ?? PRODUCTION_UPDATE_BASE_URL;
|
|
7580
8199
|
const suppressUpdateNotifications = shouldSuppressUpdateNotifications();
|
|
7581
|
-
reconcileSucceededUpdateRecord(api, logger, resolveTargetDir(api));
|
|
7582
8200
|
function clearPendingUpdateNotice() {
|
|
7583
8201
|
pendingUpdate = null;
|
|
7584
8202
|
pendingGatewayUpdate = null;
|
|
@@ -7610,42 +8228,21 @@ function registerAutoUpdate(api, logger, config, getBroadcast, rememberBroadcast
|
|
|
7610
8228
|
api.on("before_prompt_build", () => {
|
|
7611
8229
|
if (suppressUpdateNotifications)
|
|
7612
8230
|
return;
|
|
7613
|
-
|
|
8231
|
+
const notices = [];
|
|
8232
|
+
const staleNotice = buildStaleRuntimePromptNotice(staleRuntimeInfo, staleRuntimeEscalation);
|
|
8233
|
+
if (staleNotice) {
|
|
8234
|
+
notices.push(staleNotice);
|
|
8235
|
+
} else if (pendingUpdate) {
|
|
8236
|
+
notices.push(`[系统通知] phone-notifications 插件有新版本 ${pendingUpdate.latest} 可用` + `(当前版本 ${pendingUpdate.current})。
|
|
8237
|
+
` + `请用自然语言在合适时机告知用户,若用户同意更新,` + `调用 plugin-update tool 并传入 version 参数。` + `若用户拒绝,不再重复提醒。`);
|
|
8238
|
+
}
|
|
8239
|
+
if (notices.length === 0)
|
|
7614
8240
|
return;
|
|
7615
|
-
return {
|
|
7616
|
-
|
|
7617
|
-
`
|
|
7618
|
-
};
|
|
7619
|
-
});
|
|
7620
|
-
api.registerTool({
|
|
7621
|
-
name: "plugin-update",
|
|
7622
|
-
label: "Plugin Update",
|
|
7623
|
-
description: "将 phone-notifications 插件更新到指定版本。" + "仅在用户明确同意更新后调用。更新完成后会自动尝试重启 gateway;如宿主不支持,则需手动重启。",
|
|
7624
|
-
parameters: {
|
|
7625
|
-
type: "object",
|
|
7626
|
-
required: ["version"],
|
|
7627
|
-
additionalProperties: false,
|
|
7628
|
-
properties: {
|
|
7629
|
-
version: {
|
|
7630
|
-
type: "string",
|
|
7631
|
-
description: "目标版本号,如 1.11.0"
|
|
7632
|
-
}
|
|
7633
|
-
}
|
|
7634
|
-
},
|
|
7635
|
-
async execute(_toolCallId, params) {
|
|
7636
|
-
const { version } = params;
|
|
7637
|
-
const targetDir = resolveTargetDir(api);
|
|
7638
|
-
const result = await executeUpdate(version, api.runtime.system.runCommandWithTimeout, logger, targetDir, baseUrl);
|
|
7639
|
-
const finalResult = finalizeUpdateResult(api, logger, result, targetDir);
|
|
7640
|
-
if (finalResult.success) {
|
|
7641
|
-
clearPendingUpdateNotice();
|
|
7642
|
-
}
|
|
7643
|
-
return {
|
|
7644
|
-
content: [{ type: "text", text: finalResult.message }],
|
|
7645
|
-
details: finalResult
|
|
7646
|
-
};
|
|
7647
|
-
}
|
|
8241
|
+
return { appendSystemContext: notices.join(`
|
|
8242
|
+
|
|
8243
|
+
`) };
|
|
7648
8244
|
});
|
|
8245
|
+
registerPluginUpdateTool(api, logger, baseUrl, clearPendingUpdateNotice);
|
|
7649
8246
|
if (!suppressUpdateNotifications) {
|
|
7650
8247
|
logger.info(`自动更新已启用: channel=${channel}, baseUrl=${baseUrl}, checkIntervalHours=${config.checkIntervalHours ?? 4}`);
|
|
7651
8248
|
}
|
|
@@ -7807,6 +8404,14 @@ function registerAutoUpdate(api, logger, config, getBroadcast, rememberBroadcast
|
|
|
7807
8404
|
});
|
|
7808
8405
|
return {
|
|
7809
8406
|
start: () => {
|
|
8407
|
+
const targetDir = resolveTargetDir(api);
|
|
8408
|
+
staleRuntimeInfo = detectStalePluginRuntime(targetDir);
|
|
8409
|
+
staleRuntimeEscalation = staleRuntimeInfo.stale ? maybeEscalateStaleRuntimeRestart(logger, targetDir, {
|
|
8410
|
+
loadConfig: () => {
|
|
8411
|
+
const configApi = api.runtime.config;
|
|
8412
|
+
return configApi?.loadConfig?.();
|
|
8413
|
+
}
|
|
8414
|
+
}) : { action: "none" };
|
|
7810
8415
|
if (!suppressUpdateNotifications)
|
|
7811
8416
|
checker.start();
|
|
7812
8417
|
},
|
|
@@ -7825,17 +8430,9 @@ function registerAutoUpdate(api, logger, config, getBroadcast, rememberBroadcast
|
|
|
7825
8430
|
}
|
|
7826
8431
|
|
|
7827
8432
|
// src/plugin/auto-update.ts
|
|
7828
|
-
function
|
|
7829
|
-
const {
|
|
7830
|
-
api,
|
|
7831
|
-
config,
|
|
7832
|
-
logger,
|
|
7833
|
-
getBroadcastFn,
|
|
7834
|
-
cacheBroadcast,
|
|
7835
|
-
tunnelService
|
|
7836
|
-
} = deps;
|
|
8433
|
+
function resolveAutoUpdateConfig(config) {
|
|
7837
8434
|
const envName = loadEnvName();
|
|
7838
|
-
|
|
8435
|
+
return {
|
|
7839
8436
|
...config.autoUpdate,
|
|
7840
8437
|
channel: resolveUpdateChannel({
|
|
7841
8438
|
configuredChannel: config.autoUpdate?.channel,
|
|
@@ -7847,6 +8444,27 @@ function registerAutoUpdateLifecycle(deps) {
|
|
|
7847
8444
|
envName
|
|
7848
8445
|
})
|
|
7849
8446
|
};
|
|
8447
|
+
}
|
|
8448
|
+
function registerAutoUpdateAgentTool({
|
|
8449
|
+
api,
|
|
8450
|
+
config,
|
|
8451
|
+
logger
|
|
8452
|
+
}) {
|
|
8453
|
+
const autoUpdateConfig = resolveAutoUpdateConfig(config);
|
|
8454
|
+
if (autoUpdateConfig.enabled === false)
|
|
8455
|
+
return;
|
|
8456
|
+
registerPluginUpdateTool(api, logger, autoUpdateConfig.baseUrl);
|
|
8457
|
+
}
|
|
8458
|
+
function registerAutoUpdateLifecycle(deps) {
|
|
8459
|
+
const {
|
|
8460
|
+
api,
|
|
8461
|
+
config,
|
|
8462
|
+
logger,
|
|
8463
|
+
getBroadcastFn,
|
|
8464
|
+
cacheBroadcast,
|
|
8465
|
+
tunnelService
|
|
8466
|
+
} = deps;
|
|
8467
|
+
const autoUpdateConfig = resolveAutoUpdateConfig(config);
|
|
7850
8468
|
const autoUpdateLifecycle = registerAutoUpdate(api, logger, autoUpdateConfig, getBroadcastFn, cacheBroadcast, tunnelService ? {
|
|
7851
8469
|
notifyUpdateAvailable: (update) => tunnelService.notifyUpdateAvailable(update),
|
|
7852
8470
|
clearPendingUpdate: () => tunnelService.clearPendingUpdate()
|
|
@@ -7865,10 +8483,10 @@ function registerAutoUpdateLifecycle(deps) {
|
|
|
7865
8483
|
}
|
|
7866
8484
|
|
|
7867
8485
|
// src/plugin/cli.ts
|
|
7868
|
-
var
|
|
8486
|
+
var import_node_path21 = require("node:path");
|
|
7869
8487
|
|
|
7870
8488
|
// src/cli/auth.ts
|
|
7871
|
-
var
|
|
8489
|
+
var import_node_fs14 = require("node:fs");
|
|
7872
8490
|
function registerAuthCli(program) {
|
|
7873
8491
|
const auth = program.command("auth").description("用户认证管理");
|
|
7874
8492
|
auth.command("set-api-key <apiKey>").description("设置用户 API Key(持久化到本地配置)").action((apiKey) => {
|
|
@@ -7902,13 +8520,13 @@ function registerAuthCli(program) {
|
|
|
7902
8520
|
});
|
|
7903
8521
|
auth.command("clear").description("清除已保存的认证信息").action(() => {
|
|
7904
8522
|
const path = credentialsPath();
|
|
7905
|
-
if (
|
|
8523
|
+
if (import_node_fs14.existsSync(path)) {
|
|
7906
8524
|
const creds = readCredentials();
|
|
7907
8525
|
delete creds.apiKey;
|
|
7908
8526
|
delete creds.apiKeys;
|
|
7909
8527
|
delete creds.token;
|
|
7910
8528
|
if (Object.keys(creds).length === 0) {
|
|
7911
|
-
|
|
8529
|
+
import_node_fs14.rmSync(path, { force: true });
|
|
7912
8530
|
} else {
|
|
7913
8531
|
writeCredentials(creds);
|
|
7914
8532
|
}
|
|
@@ -8053,9 +8671,9 @@ function registerNtfSummary(ntf, ctx) {
|
|
|
8053
8671
|
}
|
|
8054
8672
|
|
|
8055
8673
|
// src/cli/ntf-summary-job.ts
|
|
8056
|
-
var
|
|
8057
|
-
var
|
|
8058
|
-
var
|
|
8674
|
+
var import_node_crypto4 = require("node:crypto");
|
|
8675
|
+
var import_node_fs15 = require("node:fs");
|
|
8676
|
+
var import_node_path14 = require("node:path");
|
|
8059
8677
|
var SUMMARY_ROOT = ".summaries";
|
|
8060
8678
|
var JOBS_DIR = "jobs";
|
|
8061
8679
|
var CHUNKS_DIR = "chunks";
|
|
@@ -8175,7 +8793,7 @@ function nowIso2() {
|
|
|
8175
8793
|
}
|
|
8176
8794
|
function createJobId() {
|
|
8177
8795
|
const stamp = new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
|
|
8178
|
-
return `${stamp}-${
|
|
8796
|
+
return `${stamp}-${import_node_crypto4.randomUUID().slice(0, 8)}`;
|
|
8179
8797
|
}
|
|
8180
8798
|
function assertJobId(id) {
|
|
8181
8799
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,100}$/.test(id)) {
|
|
@@ -8183,24 +8801,24 @@ function assertJobId(id) {
|
|
|
8183
8801
|
}
|
|
8184
8802
|
}
|
|
8185
8803
|
function summaryJobsDir(notificationsDir) {
|
|
8186
|
-
return
|
|
8804
|
+
return import_node_path14.join(notificationsDir, SUMMARY_ROOT, JOBS_DIR);
|
|
8187
8805
|
}
|
|
8188
8806
|
function summaryJobDir(notificationsDir, id) {
|
|
8189
8807
|
assertJobId(id);
|
|
8190
|
-
return
|
|
8808
|
+
return import_node_path14.join(summaryJobsDir(notificationsDir), id);
|
|
8191
8809
|
}
|
|
8192
8810
|
function metaPath(jobDir) {
|
|
8193
|
-
return
|
|
8811
|
+
return import_node_path14.join(jobDir, "meta.json");
|
|
8194
8812
|
}
|
|
8195
8813
|
function chunkPath(jobDir, chunkId) {
|
|
8196
|
-
return
|
|
8814
|
+
return import_node_path14.join(jobDir, CHUNKS_DIR, `${chunkId}.json`);
|
|
8197
8815
|
}
|
|
8198
8816
|
function writeJson(path, value) {
|
|
8199
|
-
|
|
8817
|
+
import_node_fs15.writeFileSync(path, JSON.stringify(value, null, 2), "utf-8");
|
|
8200
8818
|
}
|
|
8201
8819
|
function readJson(path) {
|
|
8202
8820
|
try {
|
|
8203
|
-
return JSON.parse(
|
|
8821
|
+
return JSON.parse(import_node_fs15.readFileSync(path, "utf-8"));
|
|
8204
8822
|
} catch {
|
|
8205
8823
|
exitError("READ_FAILED", `无法读取 JSON: ${path}`);
|
|
8206
8824
|
}
|
|
@@ -8208,7 +8826,7 @@ function readJson(path) {
|
|
|
8208
8826
|
function readMeta2(notificationsDir, id) {
|
|
8209
8827
|
const dir = summaryJobDir(notificationsDir, id);
|
|
8210
8828
|
const path = metaPath(dir);
|
|
8211
|
-
if (!
|
|
8829
|
+
if (!import_node_fs15.existsSync(path)) {
|
|
8212
8830
|
exitError("JOB_NOT_FOUND", `summary job 不存在: ${id}`);
|
|
8213
8831
|
}
|
|
8214
8832
|
const meta = readJson(path);
|
|
@@ -8296,7 +8914,7 @@ function resolveSummaryText(opts) {
|
|
|
8296
8914
|
if (!hasSummary && !hasSummaryFile) {
|
|
8297
8915
|
exitError("INVALID_COMMIT", "必须提供 --summary 或 --summary-file");
|
|
8298
8916
|
}
|
|
8299
|
-
const text = hasSummary ? opts.summary :
|
|
8917
|
+
const text = hasSummary ? opts.summary : import_node_fs15.readFileSync(import_node_path14.resolve(opts.summaryFile), "utf-8");
|
|
8300
8918
|
if (!text.trim()) {
|
|
8301
8919
|
exitError("INVALID_COMMIT", "分片摘要不能为空");
|
|
8302
8920
|
}
|
|
@@ -8320,7 +8938,7 @@ function buildResultMarkdown(meta, jobDir) {
|
|
|
8320
8938
|
lines.push("(未提交摘要)", "");
|
|
8321
8939
|
continue;
|
|
8322
8940
|
}
|
|
8323
|
-
lines.push(
|
|
8941
|
+
lines.push(import_node_fs15.readFileSync(import_node_path14.join(jobDir, chunk.summaryFile), "utf-8").trim(), "");
|
|
8324
8942
|
}
|
|
8325
8943
|
return lines.join(`
|
|
8326
8944
|
`).trimEnd() + `
|
|
@@ -8328,8 +8946,8 @@ function buildResultMarkdown(meta, jobDir) {
|
|
|
8328
8946
|
}
|
|
8329
8947
|
function writeChunkSummary(notificationsDir, meta, chunk, text) {
|
|
8330
8948
|
const jobDir = summaryJobDir(notificationsDir, meta.id);
|
|
8331
|
-
const relativeSummaryFile =
|
|
8332
|
-
|
|
8949
|
+
const relativeSummaryFile = import_node_path14.join(SUMMARIES_DIR, `${chunk.id}.md`);
|
|
8950
|
+
import_node_fs15.writeFileSync(import_node_path14.join(jobDir, relativeSummaryFile), text, "utf-8");
|
|
8333
8951
|
chunk.status = "done";
|
|
8334
8952
|
chunk.completedAt = nowIso2();
|
|
8335
8953
|
chunk.summaryFile = relativeSummaryFile;
|
|
@@ -8341,7 +8959,7 @@ function finalizeResultIfReady(notificationsDir, meta) {
|
|
|
8341
8959
|
}
|
|
8342
8960
|
const jobDir = summaryJobDir(notificationsDir, meta.id);
|
|
8343
8961
|
const markdown = buildResultMarkdown(meta, jobDir);
|
|
8344
|
-
|
|
8962
|
+
import_node_fs15.writeFileSync(import_node_path14.join(jobDir, RESULT_FILE), markdown, "utf-8");
|
|
8345
8963
|
meta.status = "complete";
|
|
8346
8964
|
meta.resultFile = RESULT_FILE;
|
|
8347
8965
|
return markdown;
|
|
@@ -8363,8 +8981,8 @@ function registerNtfSummaryJob(ntf, ctx) {
|
|
|
8363
8981
|
const id = createJobId();
|
|
8364
8982
|
const createdAt = nowIso2();
|
|
8365
8983
|
const jobDir = summaryJobDir(dir, id);
|
|
8366
|
-
|
|
8367
|
-
|
|
8984
|
+
import_node_fs15.mkdirSync(import_node_path14.join(jobDir, CHUNKS_DIR), { recursive: true });
|
|
8985
|
+
import_node_fs15.mkdirSync(import_node_path14.join(jobDir, SUMMARIES_DIR), { recursive: true });
|
|
8368
8986
|
const chunks = [];
|
|
8369
8987
|
for (let start = 0;start < notifications.length; start += chunkSize) {
|
|
8370
8988
|
const rawChunk = notifications.slice(start, start + chunkSize);
|
|
@@ -8658,25 +9276,25 @@ function registerNtfStats(ntf, ctx) {
|
|
|
8658
9276
|
}
|
|
8659
9277
|
|
|
8660
9278
|
// src/cli/ntf-sync.ts
|
|
8661
|
-
var
|
|
8662
|
-
var
|
|
8663
|
-
var
|
|
9279
|
+
var import_node_child_process3 = require("node:child_process");
|
|
9280
|
+
var import_node_fs16 = require("node:fs");
|
|
9281
|
+
var import_node_path15 = require("node:path");
|
|
8664
9282
|
var SYNC_FETCH_LIMIT = 100;
|
|
8665
9283
|
function checkpointPath(dir) {
|
|
8666
|
-
return
|
|
9284
|
+
return import_node_path15.join(dir, ".checkpoint.json");
|
|
8667
9285
|
}
|
|
8668
9286
|
function readCheckpoint(dir) {
|
|
8669
9287
|
const p = checkpointPath(dir);
|
|
8670
|
-
if (!
|
|
9288
|
+
if (!import_node_fs16.existsSync(p))
|
|
8671
9289
|
return {};
|
|
8672
9290
|
try {
|
|
8673
|
-
return JSON.parse(
|
|
9291
|
+
return JSON.parse(import_node_fs16.readFileSync(p, "utf-8"));
|
|
8674
9292
|
} catch {
|
|
8675
9293
|
return {};
|
|
8676
9294
|
}
|
|
8677
9295
|
}
|
|
8678
9296
|
function writeCheckpoint(dir, data) {
|
|
8679
|
-
|
|
9297
|
+
import_node_fs16.writeFileSync(checkpointPath(dir), JSON.stringify(data, null, 2), "utf-8");
|
|
8680
9298
|
}
|
|
8681
9299
|
function validateDateKey(value, optionName) {
|
|
8682
9300
|
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
@@ -8730,7 +9348,7 @@ function isDateInScope(dateKey, scope) {
|
|
|
8730
9348
|
var MEMORY_PLUGIN_KEYWORD = "memory-lancedb-ultra";
|
|
8731
9349
|
function detectMemoryBackend() {
|
|
8732
9350
|
try {
|
|
8733
|
-
const out =
|
|
9351
|
+
const out = import_node_child_process3.execFileSync("openclaw", ["plugins", "list"], {
|
|
8734
9352
|
encoding: "utf-8",
|
|
8735
9353
|
timeout: 15000,
|
|
8736
9354
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -8947,8 +9565,8 @@ function registerNtfSync(ntf, ctx) {
|
|
|
8947
9565
|
}
|
|
8948
9566
|
|
|
8949
9567
|
// src/cli/ntf-monitor.ts
|
|
8950
|
-
var
|
|
8951
|
-
var
|
|
9568
|
+
var import_node_fs17 = require("node:fs");
|
|
9569
|
+
var import_node_path16 = require("node:path");
|
|
8952
9570
|
|
|
8953
9571
|
// src/monitor/fetch-gen.ts
|
|
8954
9572
|
function generateFetchPy(name, matchRules) {
|
|
@@ -9035,20 +9653,20 @@ function tasksDir(ctx) {
|
|
|
9035
9653
|
const base = ctx.workspaceDir || ctx.stateDir;
|
|
9036
9654
|
if (!base)
|
|
9037
9655
|
throw new Error("workspaceDir and stateDir both unavailable");
|
|
9038
|
-
return
|
|
9656
|
+
return import_node_path16.join(base, "tasks");
|
|
9039
9657
|
}
|
|
9040
9658
|
function readMeta3(taskDir) {
|
|
9041
|
-
const metaPath2 =
|
|
9042
|
-
if (!
|
|
9659
|
+
const metaPath2 = import_node_path16.join(taskDir, "meta.json");
|
|
9660
|
+
if (!import_node_fs17.existsSync(metaPath2))
|
|
9043
9661
|
return null;
|
|
9044
9662
|
try {
|
|
9045
|
-
return JSON.parse(
|
|
9663
|
+
return JSON.parse(import_node_fs17.readFileSync(metaPath2, "utf-8"));
|
|
9046
9664
|
} catch {
|
|
9047
9665
|
return null;
|
|
9048
9666
|
}
|
|
9049
9667
|
}
|
|
9050
9668
|
function writeMeta2(taskDir, meta) {
|
|
9051
|
-
|
|
9669
|
+
import_node_fs17.writeFileSync(import_node_path16.join(taskDir, "meta.json"), JSON.stringify(meta, null, 2), "utf-8");
|
|
9052
9670
|
}
|
|
9053
9671
|
function generateReadme(name, description) {
|
|
9054
9672
|
return `# Monitor Task: ${name}
|
|
@@ -9067,30 +9685,30 @@ function registerNtfMonitor(ntf, ctx) {
|
|
|
9067
9685
|
const monitor = ntf.command("monitor").description("通知监控任务管理");
|
|
9068
9686
|
monitor.command("list").description("列出所有监控任务").action(() => {
|
|
9069
9687
|
const dir = tasksDir(ctx);
|
|
9070
|
-
if (!
|
|
9688
|
+
if (!import_node_fs17.existsSync(dir)) {
|
|
9071
9689
|
output({ ok: true, tasks: [] });
|
|
9072
9690
|
return;
|
|
9073
9691
|
}
|
|
9074
9692
|
const tasks = [];
|
|
9075
|
-
for (const entry of
|
|
9693
|
+
for (const entry of import_node_fs17.readdirSync(dir, { withFileTypes: true })) {
|
|
9076
9694
|
if (!entry.isDirectory())
|
|
9077
9695
|
continue;
|
|
9078
|
-
const meta = readMeta3(
|
|
9696
|
+
const meta = readMeta3(import_node_path16.join(dir, entry.name));
|
|
9079
9697
|
if (meta)
|
|
9080
9698
|
tasks.push(meta);
|
|
9081
9699
|
}
|
|
9082
9700
|
output({ ok: true, tasks });
|
|
9083
9701
|
});
|
|
9084
9702
|
monitor.command("show <name>").description("查看监控任务详情").action((name) => {
|
|
9085
|
-
const taskDir =
|
|
9703
|
+
const taskDir = import_node_path16.join(tasksDir(ctx), name);
|
|
9086
9704
|
const meta = readMeta3(taskDir);
|
|
9087
9705
|
if (!meta)
|
|
9088
9706
|
exitError("NOT_FOUND", `监控任务 '${name}' 不存在`);
|
|
9089
|
-
const checkpointPath2 =
|
|
9707
|
+
const checkpointPath2 = import_node_path16.join(taskDir, "checkpoint.json");
|
|
9090
9708
|
let checkpoint = {};
|
|
9091
|
-
if (
|
|
9709
|
+
if (import_node_fs17.existsSync(checkpointPath2)) {
|
|
9092
9710
|
try {
|
|
9093
|
-
checkpoint = JSON.parse(
|
|
9711
|
+
checkpoint = JSON.parse(import_node_fs17.readFileSync(checkpointPath2, "utf-8"));
|
|
9094
9712
|
} catch {}
|
|
9095
9713
|
}
|
|
9096
9714
|
output({
|
|
@@ -9103,8 +9721,8 @@ function registerNtfMonitor(ntf, ctx) {
|
|
|
9103
9721
|
});
|
|
9104
9722
|
monitor.command("create <name>").description("创建监控任务").requiredOption("--description <text>", "任务描述").requiredOption("--match-rules <json>", "匹配规则 JSON").requiredOption("--schedule <cron>", "cron 表达式").action((name, opts) => {
|
|
9105
9723
|
const dir = tasksDir(ctx);
|
|
9106
|
-
const taskDir =
|
|
9107
|
-
if (
|
|
9724
|
+
const taskDir = import_node_path16.join(dir, name);
|
|
9725
|
+
if (import_node_fs17.existsSync(taskDir)) {
|
|
9108
9726
|
exitError("ALREADY_EXISTS", `监控任务 '${name}' 已存在`);
|
|
9109
9727
|
}
|
|
9110
9728
|
let matchRules;
|
|
@@ -9113,7 +9731,7 @@ function registerNtfMonitor(ntf, ctx) {
|
|
|
9113
9731
|
} catch {
|
|
9114
9732
|
exitError("VALIDATION_FAILED", "match-rules 必须是合法的 JSON");
|
|
9115
9733
|
}
|
|
9116
|
-
|
|
9734
|
+
import_node_fs17.mkdirSync(taskDir, { recursive: true });
|
|
9117
9735
|
const meta = {
|
|
9118
9736
|
name,
|
|
9119
9737
|
description: opts.description,
|
|
@@ -9123,8 +9741,8 @@ function registerNtfMonitor(ntf, ctx) {
|
|
|
9123
9741
|
createdAt: new Date().toISOString()
|
|
9124
9742
|
};
|
|
9125
9743
|
writeMeta2(taskDir, meta);
|
|
9126
|
-
|
|
9127
|
-
|
|
9744
|
+
import_node_fs17.writeFileSync(import_node_path16.join(taskDir, "fetch.py"), generateFetchPy(name, matchRules), "utf-8");
|
|
9745
|
+
import_node_fs17.writeFileSync(import_node_path16.join(taskDir, "README.md"), generateReadme(name, opts.description), "utf-8");
|
|
9128
9746
|
output({
|
|
9129
9747
|
ok: true,
|
|
9130
9748
|
name,
|
|
@@ -9148,8 +9766,8 @@ function registerNtfMonitor(ntf, ctx) {
|
|
|
9148
9766
|
});
|
|
9149
9767
|
});
|
|
9150
9768
|
monitor.command("delete <name>").description("删除监控任务").option("--yes", "跳过确认").action((name, opts) => {
|
|
9151
|
-
const taskDir =
|
|
9152
|
-
if (!
|
|
9769
|
+
const taskDir = import_node_path16.join(tasksDir(ctx), name);
|
|
9770
|
+
if (!import_node_fs17.existsSync(taskDir)) {
|
|
9153
9771
|
exitError("NOT_FOUND", `监控任务 '${name}' 不存在`);
|
|
9154
9772
|
}
|
|
9155
9773
|
if (!opts.yes) {
|
|
@@ -9162,7 +9780,7 @@ function registerNtfMonitor(ntf, ctx) {
|
|
|
9162
9780
|
});
|
|
9163
9781
|
process.exit(1);
|
|
9164
9782
|
}
|
|
9165
|
-
|
|
9783
|
+
import_node_fs17.rmSync(taskDir, { recursive: true, force: true });
|
|
9166
9784
|
output({
|
|
9167
9785
|
ok: true,
|
|
9168
9786
|
name,
|
|
@@ -9174,7 +9792,7 @@ function registerNtfMonitor(ntf, ctx) {
|
|
|
9174
9792
|
});
|
|
9175
9793
|
});
|
|
9176
9794
|
monitor.command("enable <name>").description("启用监控任务").action((name) => {
|
|
9177
|
-
const taskDir =
|
|
9795
|
+
const taskDir = import_node_path16.join(tasksDir(ctx), name);
|
|
9178
9796
|
const meta = readMeta3(taskDir);
|
|
9179
9797
|
if (!meta)
|
|
9180
9798
|
exitError("NOT_FOUND", `监控任务 '${name}' 不存在`);
|
|
@@ -9183,7 +9801,7 @@ function registerNtfMonitor(ntf, ctx) {
|
|
|
9183
9801
|
output({ ok: true, name, enabled: true });
|
|
9184
9802
|
});
|
|
9185
9803
|
monitor.command("disable <name>").description("暂停监控任务").action((name) => {
|
|
9186
|
-
const taskDir =
|
|
9804
|
+
const taskDir = import_node_path16.join(tasksDir(ctx), name);
|
|
9187
9805
|
const meta = readMeta3(taskDir);
|
|
9188
9806
|
if (!meta)
|
|
9189
9807
|
exitError("NOT_FOUND", `监控任务 '${name}' 不存在`);
|
|
@@ -9193,63 +9811,6 @@ function registerNtfMonitor(ntf, ctx) {
|
|
|
9193
9811
|
});
|
|
9194
9812
|
}
|
|
9195
9813
|
|
|
9196
|
-
// src/light/sender.ts
|
|
9197
|
-
var import_node_crypto4 = require("node:crypto");
|
|
9198
|
-
async function sendLightEffect(apiKey, segments, logger, repeatInput, reason, title) {
|
|
9199
|
-
const apiUrl = getEnvUrls().lightApiUrl;
|
|
9200
|
-
const appKey = "phone-notifications";
|
|
9201
|
-
const templateId = "1990771146010017800";
|
|
9202
|
-
const resolvedTitle = resolveLightTitle(title, reason, segments);
|
|
9203
|
-
logger?.info(`Light sender: apiUrl=${apiUrl ?? "UNSET"}, appKey=${appKey ? appKey.substring(0, 8) + "…" : "UNSET"}, templateId=${templateId ?? "UNSET"}, apiKey=${apiKey ? apiKey.substring(0, 20) + "…" : "EMPTY"}, title=${resolvedTitle}, reason=${reason ?? ""}, segments=${JSON.stringify(segments)}`);
|
|
9204
|
-
if (!apiUrl || !appKey || !templateId) {
|
|
9205
|
-
return {
|
|
9206
|
-
ok: false,
|
|
9207
|
-
error: "灯效 API 未配置,请确认构建时已封装 OPENCLAW_HOST_* / LIGHT_APP_KEY / LIGHT_TEMPLATE_ID"
|
|
9208
|
-
};
|
|
9209
|
-
}
|
|
9210
|
-
let bizContent;
|
|
9211
|
-
try {
|
|
9212
|
-
bizContent = buildLightEffectApnsBody(segments, repeatInput, reason);
|
|
9213
|
-
} catch (error) {
|
|
9214
|
-
return { ok: false, error: error?.message ?? String(error) };
|
|
9215
|
-
}
|
|
9216
|
-
const bizUniqueId = import_node_crypto4.randomUUID();
|
|
9217
|
-
const requestBody = {
|
|
9218
|
-
appKey,
|
|
9219
|
-
bizMap: { noticeType: "APP_NOTIFICATION_IMPORTANT", title: resolvedTitle, reason },
|
|
9220
|
-
bizUniqueId,
|
|
9221
|
-
paramsMap: { bizContent },
|
|
9222
|
-
pushType: "SPECIFY_PUSH",
|
|
9223
|
-
templateId
|
|
9224
|
-
};
|
|
9225
|
-
logger?.info(`Light sender: POST ${apiUrl}, bizUniqueId=${bizUniqueId}, body=${JSON.stringify(requestBody).substring(0, 500)}`);
|
|
9226
|
-
const res = await fetch(apiUrl, {
|
|
9227
|
-
method: "POST",
|
|
9228
|
-
headers: {
|
|
9229
|
-
"Content-Type": "application/json",
|
|
9230
|
-
"X-Api-Key-Id": apiKey.startsWith("Bearer ") ? apiKey.slice("Bearer ".length) : apiKey
|
|
9231
|
-
},
|
|
9232
|
-
body: JSON.stringify(requestBody)
|
|
9233
|
-
});
|
|
9234
|
-
const resBody = await res.text();
|
|
9235
|
-
if (!res.ok) {
|
|
9236
|
-
logger?.warn(`Light sender: FAILED ${res.status}, url=${apiUrl}, resBody=${resBody.substring(0, 500)}`);
|
|
9237
|
-
return { ok: false, status: res.status, error: resBody };
|
|
9238
|
-
}
|
|
9239
|
-
logger?.info(`Light sender: OK bizUniqueId=${bizUniqueId}, resBody=${resBody.substring(0, 200)}`);
|
|
9240
|
-
return { ok: true, bizUniqueId, response: JSON.parse(resBody) };
|
|
9241
|
-
}
|
|
9242
|
-
function resolveLightTitle(title, reason, segments) {
|
|
9243
|
-
const trimmedTitle = title?.trim();
|
|
9244
|
-
if (trimmedTitle)
|
|
9245
|
-
return trimmedTitle;
|
|
9246
|
-
const trimmedReason = reason?.trim();
|
|
9247
|
-
if (trimmedReason)
|
|
9248
|
-
return trimmedReason;
|
|
9249
|
-
const modeDesc = segments.map((segment) => segment.mode).join("+");
|
|
9250
|
-
return `Effect: ${modeDesc || "custom"}`;
|
|
9251
|
-
}
|
|
9252
|
-
|
|
9253
9814
|
// src/cli/light-send.ts
|
|
9254
9815
|
function registerLightSend(light) {
|
|
9255
9816
|
light.command("send").description("发送灯效指令到硬件设备").requiredOption("--segments <json>", "灯效参数 JSON").option("--repeat", "无限循环播放(默认仅播放一轮)").option("--repeat-times <n>", "整条组合重复次数:0=无限,1=一轮;当前 ANCS 路径不支持 >=2").action(async (opts) => {
|
|
@@ -9279,8 +9840,8 @@ function registerLightSend(light) {
|
|
|
9279
9840
|
}
|
|
9280
9841
|
|
|
9281
9842
|
// src/cli/light-setup-tools.ts
|
|
9282
|
-
var
|
|
9283
|
-
var
|
|
9843
|
+
var import_node_fs18 = require("node:fs");
|
|
9844
|
+
var import_node_path17 = require("node:path");
|
|
9284
9845
|
function isObject(value) {
|
|
9285
9846
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
9286
9847
|
}
|
|
@@ -9326,7 +9887,7 @@ function upsertToolsForPolicy(policy, tools) {
|
|
|
9326
9887
|
function resolveConfigPath2() {
|
|
9327
9888
|
return resolveConfigPath();
|
|
9328
9889
|
}
|
|
9329
|
-
var LIGHT_TOOLS = [...LIGHT_RULE_TOOL_NAME_LIST];
|
|
9890
|
+
var LIGHT_TOOLS = ["light_control", ...LIGHT_RULE_TOOL_NAME_LIST];
|
|
9330
9891
|
function upsertLightRuleToolsAlsoAllow(cfg) {
|
|
9331
9892
|
if (!isObject(cfg.tools))
|
|
9332
9893
|
cfg.tools = {};
|
|
@@ -9346,14 +9907,14 @@ function upsertLightRuleToolsAlsoAllow(cfg) {
|
|
|
9346
9907
|
return { globalChanged, mainAgentChanged };
|
|
9347
9908
|
}
|
|
9348
9909
|
function registerLightSetupTools(light) {
|
|
9349
|
-
light.command("setup").description("
|
|
9910
|
+
light.command("setup").description("自动放行一次性灯效与云端灯效规则工具(兼容 tools.allow / tools.alsoAllow)").action(() => {
|
|
9350
9911
|
const configPath = resolveConfigPath2();
|
|
9351
|
-
if (!
|
|
9912
|
+
if (!import_node_fs18.existsSync(configPath)) {
|
|
9352
9913
|
exitError("CONFIG_NOT_FOUND", `未找到配置文件: ${configPath}`);
|
|
9353
9914
|
}
|
|
9354
9915
|
let cfg = {};
|
|
9355
9916
|
try {
|
|
9356
|
-
const raw =
|
|
9917
|
+
const raw = import_node_fs18.readFileSync(configPath, "utf-8");
|
|
9357
9918
|
const parsed = JSON.parse(raw);
|
|
9358
9919
|
if (isObject(parsed))
|
|
9359
9920
|
cfg = parsed;
|
|
@@ -9362,8 +9923,8 @@ function registerLightSetupTools(light) {
|
|
|
9362
9923
|
}
|
|
9363
9924
|
const result = upsertLightRuleToolsAlsoAllow(cfg);
|
|
9364
9925
|
try {
|
|
9365
|
-
|
|
9366
|
-
|
|
9926
|
+
import_node_fs18.mkdirSync(import_node_path17.dirname(configPath), { recursive: true });
|
|
9927
|
+
import_node_fs18.writeFileSync(configPath, JSON.stringify(cfg, null, 2) + `
|
|
9367
9928
|
`, "utf-8");
|
|
9368
9929
|
} catch (err2) {
|
|
9369
9930
|
exitError("WRITE_FAILED", `写入配置失败: ${err2?.message ?? String(err2)}`);
|
|
@@ -9382,10 +9943,10 @@ function registerLightSetupTools(light) {
|
|
|
9382
9943
|
}
|
|
9383
9944
|
|
|
9384
9945
|
// src/tunnel/status.ts
|
|
9385
|
-
var
|
|
9386
|
-
var
|
|
9387
|
-
var TUNNEL_STATUS_REL_PATH =
|
|
9388
|
-
var TUNNEL_LOCK_REL_PATH =
|
|
9946
|
+
var import_node_fs19 = require("node:fs");
|
|
9947
|
+
var import_node_path18 = require("node:path");
|
|
9948
|
+
var TUNNEL_STATUS_REL_PATH = import_node_path18.join("plugins", "phone-notifications", "tunnel-status.json");
|
|
9949
|
+
var TUNNEL_LOCK_REL_PATH = import_node_path18.join("plugins", "phone-notifications", "relay-tunnel.lock");
|
|
9389
9950
|
function isTunnelState(value) {
|
|
9390
9951
|
return value === "connected" || value === "connecting" || value === "disconnected" || value === "stopped";
|
|
9391
9952
|
}
|
|
@@ -9434,9 +9995,9 @@ function staleStatusMessage(status, lock) {
|
|
|
9434
9995
|
return `${prefix},但当前运行锁启动于 ${lock.startedAt},晚于状态时间,说明状态未随新进程刷新。请重启 openclaw 主进程。`;
|
|
9435
9996
|
}
|
|
9436
9997
|
function assessTunnelStatus(stateDir) {
|
|
9437
|
-
const statusFilePath =
|
|
9438
|
-
const lockFilePath =
|
|
9439
|
-
if (!
|
|
9998
|
+
const statusFilePath = import_node_path18.join(stateDir, TUNNEL_STATUS_REL_PATH);
|
|
9999
|
+
const lockFilePath = import_node_path18.join(stateDir, TUNNEL_LOCK_REL_PATH);
|
|
10000
|
+
if (!import_node_fs19.existsSync(statusFilePath)) {
|
|
9440
10001
|
return {
|
|
9441
10002
|
status: null,
|
|
9442
10003
|
issueCode: "STATUS_NOT_FOUND",
|
|
@@ -9444,7 +10005,7 @@ function assessTunnelStatus(stateDir) {
|
|
|
9444
10005
|
statusFilePath,
|
|
9445
10006
|
lockFilePath,
|
|
9446
10007
|
lock: {
|
|
9447
|
-
exists:
|
|
10008
|
+
exists: import_node_fs19.existsSync(lockFilePath),
|
|
9448
10009
|
pid: null,
|
|
9449
10010
|
startedAt: null,
|
|
9450
10011
|
active: null
|
|
@@ -9453,7 +10014,7 @@ function assessTunnelStatus(stateDir) {
|
|
|
9453
10014
|
}
|
|
9454
10015
|
let rawStatus;
|
|
9455
10016
|
try {
|
|
9456
|
-
rawStatus = JSON.parse(
|
|
10017
|
+
rawStatus = JSON.parse(import_node_fs19.readFileSync(statusFilePath, "utf-8"));
|
|
9457
10018
|
} catch {
|
|
9458
10019
|
return {
|
|
9459
10020
|
status: null,
|
|
@@ -9462,7 +10023,7 @@ function assessTunnelStatus(stateDir) {
|
|
|
9462
10023
|
statusFilePath,
|
|
9463
10024
|
lockFilePath,
|
|
9464
10025
|
lock: {
|
|
9465
|
-
exists:
|
|
10026
|
+
exists: import_node_fs19.existsSync(lockFilePath),
|
|
9466
10027
|
pid: null,
|
|
9467
10028
|
startedAt: null,
|
|
9468
10029
|
active: null
|
|
@@ -9477,7 +10038,7 @@ function assessTunnelStatus(stateDir) {
|
|
|
9477
10038
|
statusFilePath,
|
|
9478
10039
|
lockFilePath,
|
|
9479
10040
|
lock: {
|
|
9480
|
-
exists:
|
|
10041
|
+
exists: import_node_fs19.existsSync(lockFilePath),
|
|
9481
10042
|
pid: null,
|
|
9482
10043
|
startedAt: null,
|
|
9483
10044
|
active: null
|
|
@@ -9485,11 +10046,11 @@ function assessTunnelStatus(stateDir) {
|
|
|
9485
10046
|
};
|
|
9486
10047
|
}
|
|
9487
10048
|
const status = rawStatus;
|
|
9488
|
-
const lockExists =
|
|
10049
|
+
const lockExists = import_node_fs19.existsSync(lockFilePath);
|
|
9489
10050
|
let lockInfo = null;
|
|
9490
10051
|
if (lockExists) {
|
|
9491
10052
|
try {
|
|
9492
|
-
lockInfo = parseTunnelLockInfo(JSON.parse(
|
|
10053
|
+
lockInfo = parseTunnelLockInfo(JSON.parse(import_node_fs19.readFileSync(lockFilePath, "utf-8")));
|
|
9493
10054
|
} catch {
|
|
9494
10055
|
lockInfo = null;
|
|
9495
10056
|
}
|
|
@@ -9592,12 +10153,12 @@ function registerNtfStoragePath(ntf, ctx) {
|
|
|
9592
10153
|
}
|
|
9593
10154
|
|
|
9594
10155
|
// src/cli/log-search.ts
|
|
9595
|
-
var
|
|
9596
|
-
var
|
|
10156
|
+
var import_node_fs20 = require("node:fs");
|
|
10157
|
+
var import_node_path19 = require("node:path");
|
|
9597
10158
|
function resolveLogsDir(ctx) {
|
|
9598
10159
|
if (ctx.stateDir) {
|
|
9599
|
-
const dir =
|
|
9600
|
-
if (
|
|
10160
|
+
const dir = import_node_path19.join(ctx.stateDir, "plugins", "phone-notifications", "logs");
|
|
10161
|
+
if (import_node_fs20.existsSync(dir))
|
|
9601
10162
|
return dir;
|
|
9602
10163
|
}
|
|
9603
10164
|
return null;
|
|
@@ -9605,7 +10166,7 @@ function resolveLogsDir(ctx) {
|
|
|
9605
10166
|
function listLogDateKeys(dir) {
|
|
9606
10167
|
const pattern = /^(\d{4}-\d{2}-\d{2})\.log$/;
|
|
9607
10168
|
const keys = [];
|
|
9608
|
-
for (const entry of
|
|
10169
|
+
for (const entry of import_node_fs20.readdirSync(dir, { withFileTypes: true })) {
|
|
9609
10170
|
if (!entry.isFile())
|
|
9610
10171
|
continue;
|
|
9611
10172
|
const m = pattern.exec(entry.name);
|
|
@@ -9615,10 +10176,10 @@ function listLogDateKeys(dir) {
|
|
|
9615
10176
|
return keys.sort().reverse();
|
|
9616
10177
|
}
|
|
9617
10178
|
function collectLogLines(dir, dateKey, keyword, limit, collected) {
|
|
9618
|
-
const filePath =
|
|
9619
|
-
if (!
|
|
10179
|
+
const filePath = import_node_path19.join(dir, `${dateKey}.log`);
|
|
10180
|
+
if (!import_node_fs20.existsSync(filePath))
|
|
9620
10181
|
return;
|
|
9621
|
-
const content =
|
|
10182
|
+
const content = import_node_fs20.readFileSync(filePath, "utf-8");
|
|
9622
10183
|
const lowerKeyword = keyword?.toLowerCase();
|
|
9623
10184
|
for (const line of content.split(`
|
|
9624
10185
|
`)) {
|
|
@@ -9654,7 +10215,7 @@ function registerLogSearch(ntf, ctx) {
|
|
|
9654
10215
|
}
|
|
9655
10216
|
|
|
9656
10217
|
// src/cli/env.ts
|
|
9657
|
-
var
|
|
10218
|
+
var import_node_child_process4 = require("node:child_process");
|
|
9658
10219
|
var GATEWAY_RESTART_TIMEOUT_MS = 30000;
|
|
9659
10220
|
var NOOP_LOGGER = {
|
|
9660
10221
|
info() {},
|
|
@@ -9705,7 +10266,7 @@ function triggerGatewayReloadAfterEnvSwitch(deps = {}) {
|
|
|
9705
10266
|
}
|
|
9706
10267
|
const hostCli = deps.hostCli?.trim() || process.env.HOST_CLI?.trim() || "openclaw";
|
|
9707
10268
|
const command = `${hostCli} gateway restart`;
|
|
9708
|
-
const run = deps.spawnSync ??
|
|
10269
|
+
const run = deps.spawnSync ?? import_node_child_process4.spawnSync;
|
|
9709
10270
|
const result = run(hostCli, ["gateway", "restart"], {
|
|
9710
10271
|
encoding: "utf-8",
|
|
9711
10272
|
stdio: "pipe",
|
|
@@ -9759,11 +10320,11 @@ function registerEnvCli(ntf, deps = {}) {
|
|
|
9759
10320
|
}
|
|
9760
10321
|
|
|
9761
10322
|
// src/cli/doctor.ts
|
|
9762
|
-
var
|
|
10323
|
+
var import_node_fs23 = require("node:fs");
|
|
9763
10324
|
var import_node_readline = require("node:readline");
|
|
9764
10325
|
|
|
9765
10326
|
// src/cli/doctor/check-dangerous-flags.ts
|
|
9766
|
-
var
|
|
10327
|
+
var import_node_fs21 = require("node:fs");
|
|
9767
10328
|
function isObject2(v) {
|
|
9768
10329
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
9769
10330
|
}
|
|
@@ -9783,13 +10344,13 @@ var checkDangerousFlags = ({ cfg, configPath }) => {
|
|
|
9783
10344
|
detail: "这会关闭 Control UI 的设备身份验证,任何人都可以访问控制面板。",
|
|
9784
10345
|
fixDescription: "设为 false",
|
|
9785
10346
|
fix: () => {
|
|
9786
|
-
const raw =
|
|
10347
|
+
const raw = import_node_fs21.readFileSync(configPath, "utf-8");
|
|
9787
10348
|
const config = JSON.parse(raw);
|
|
9788
10349
|
const gw = config.gateway;
|
|
9789
10350
|
const cui = gw.controlUi;
|
|
9790
10351
|
cui.dangerouslyDisableDeviceAuth = false;
|
|
9791
|
-
|
|
9792
|
-
|
|
10352
|
+
import_node_fs21.copyFileSync(configPath, configPath + ".bak");
|
|
10353
|
+
import_node_fs21.writeFileSync(configPath, JSON.stringify(config, null, 2) + `
|
|
9793
10354
|
`, "utf-8");
|
|
9794
10355
|
}
|
|
9795
10356
|
};
|
|
@@ -9846,11 +10407,11 @@ function warnEmpty() {
|
|
|
9846
10407
|
}
|
|
9847
10408
|
|
|
9848
10409
|
// src/cli/doctor/check-state-dir-perms.ts
|
|
9849
|
-
var
|
|
10410
|
+
var import_node_fs22 = require("node:fs");
|
|
9850
10411
|
var checkStateDirPerms = ({ stateDir }) => {
|
|
9851
10412
|
let mode;
|
|
9852
10413
|
try {
|
|
9853
|
-
mode =
|
|
10414
|
+
mode = import_node_fs22.statSync(stateDir).mode;
|
|
9854
10415
|
} catch {
|
|
9855
10416
|
return null;
|
|
9856
10417
|
}
|
|
@@ -9865,7 +10426,7 @@ var checkStateDirPerms = ({ stateDir }) => {
|
|
|
9865
10426
|
detail: "其他用户可以读取该目录下的凭证和配置文件。",
|
|
9866
10427
|
fixDescription: "chmod 700 " + stateDir,
|
|
9867
10428
|
fix: () => {
|
|
9868
|
-
|
|
10429
|
+
import_node_fs22.chmodSync(stateDir, 448);
|
|
9869
10430
|
}
|
|
9870
10431
|
};
|
|
9871
10432
|
};
|
|
@@ -9970,6 +10531,180 @@ var checkTunnel = ({ stateDir }) => {
|
|
|
9970
10531
|
};
|
|
9971
10532
|
};
|
|
9972
10533
|
|
|
10534
|
+
// src/cli/doctor/check-local-gateway.ts
|
|
10535
|
+
var import_node_net = __toESM(require("node:net"));
|
|
10536
|
+
|
|
10537
|
+
// src/tunnel/gateway-target.ts
|
|
10538
|
+
var DEFAULT_GATEWAY_PORT = "18789";
|
|
10539
|
+
var DEFAULT_GATEWAY_HOST = "127.0.0.1";
|
|
10540
|
+
var BASE_URL_ENV_KEYS = [
|
|
10541
|
+
"PHONE_NOTIFICATIONS_GATEWAY_BASE_URL",
|
|
10542
|
+
"OPENCLAW_GATEWAY_BASE_URL",
|
|
10543
|
+
"QCLAW_GATEWAY_BASE_URL"
|
|
10544
|
+
];
|
|
10545
|
+
var PORT_ENV_KEYS = [
|
|
10546
|
+
"OPENCLAW_GATEWAY_PORT",
|
|
10547
|
+
"QCLAW_GATEWAY_PORT"
|
|
10548
|
+
];
|
|
10549
|
+
function trimToUndefined3(value) {
|
|
10550
|
+
if (typeof value !== "string")
|
|
10551
|
+
return;
|
|
10552
|
+
const trimmed = value.trim();
|
|
10553
|
+
return trimmed || undefined;
|
|
10554
|
+
}
|
|
10555
|
+
function firstEnv(env, keys) {
|
|
10556
|
+
for (const key of keys) {
|
|
10557
|
+
const value = trimToUndefined3(env[key]);
|
|
10558
|
+
if (value)
|
|
10559
|
+
return { key, value };
|
|
10560
|
+
}
|
|
10561
|
+
return;
|
|
10562
|
+
}
|
|
10563
|
+
function normalizePort(value) {
|
|
10564
|
+
const raw = typeof value === "number" && Number.isInteger(value) ? String(value) : trimToUndefined3(value);
|
|
10565
|
+
if (!raw || !/^\d+$/.test(raw))
|
|
10566
|
+
return;
|
|
10567
|
+
const port = Number.parseInt(raw, 10);
|
|
10568
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
10569
|
+
return;
|
|
10570
|
+
}
|
|
10571
|
+
return String(port);
|
|
10572
|
+
}
|
|
10573
|
+
function normalizeBaseUrl(raw) {
|
|
10574
|
+
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(raw) ? raw : `http://${raw}`;
|
|
10575
|
+
let parsed;
|
|
10576
|
+
try {
|
|
10577
|
+
parsed = new URL(withScheme);
|
|
10578
|
+
} catch {
|
|
10579
|
+
return;
|
|
10580
|
+
}
|
|
10581
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
10582
|
+
return;
|
|
10583
|
+
}
|
|
10584
|
+
if (!parsed.hostname)
|
|
10585
|
+
return;
|
|
10586
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
10587
|
+
}
|
|
10588
|
+
function isGatewayTlsEnabled(config) {
|
|
10589
|
+
return config.gateway?.tls?.enabled === true;
|
|
10590
|
+
}
|
|
10591
|
+
function resolveGatewayPort(params) {
|
|
10592
|
+
const envPort = firstEnv(params.env, PORT_ENV_KEYS);
|
|
10593
|
+
const normalizedEnvPort = envPort ? normalizePort(envPort.value) : undefined;
|
|
10594
|
+
if (envPort && normalizedEnvPort) {
|
|
10595
|
+
return { port: normalizedEnvPort, source: envPort.key };
|
|
10596
|
+
}
|
|
10597
|
+
const configPort = normalizePort(params.config.gateway?.port);
|
|
10598
|
+
if (configPort) {
|
|
10599
|
+
return { port: configPort, source: "openclaw.json gateway.port" };
|
|
10600
|
+
}
|
|
10601
|
+
return { port: DEFAULT_GATEWAY_PORT, source: "default" };
|
|
10602
|
+
}
|
|
10603
|
+
function resolveLocalGatewayBaseUrl(params = {}) {
|
|
10604
|
+
const config = params.config ?? {};
|
|
10605
|
+
const env = params.env ?? process.env;
|
|
10606
|
+
const tlsEnabled = isGatewayTlsEnabled(config);
|
|
10607
|
+
const envBaseUrl = firstEnv(env, BASE_URL_ENV_KEYS);
|
|
10608
|
+
if (envBaseUrl) {
|
|
10609
|
+
const normalized = normalizeBaseUrl(envBaseUrl.value);
|
|
10610
|
+
if (normalized) {
|
|
10611
|
+
return {
|
|
10612
|
+
baseUrl: normalized,
|
|
10613
|
+
source: envBaseUrl.key,
|
|
10614
|
+
explicit: true,
|
|
10615
|
+
tlsEnabled
|
|
10616
|
+
};
|
|
10617
|
+
}
|
|
10618
|
+
}
|
|
10619
|
+
const { port, source } = resolveGatewayPort({ config, env });
|
|
10620
|
+
const baseUrl = `http://${DEFAULT_GATEWAY_HOST}:${port}`;
|
|
10621
|
+
const warning = envBaseUrl ? `忽略无效的 ${envBaseUrl.key}=${envBaseUrl.value}` : undefined;
|
|
10622
|
+
return {
|
|
10623
|
+
baseUrl,
|
|
10624
|
+
source,
|
|
10625
|
+
explicit: false,
|
|
10626
|
+
tlsEnabled,
|
|
10627
|
+
warning
|
|
10628
|
+
};
|
|
10629
|
+
}
|
|
10630
|
+
function resolveLocalGatewayTcpTarget(baseUrl) {
|
|
10631
|
+
let parsed;
|
|
10632
|
+
try {
|
|
10633
|
+
parsed = new URL(baseUrl);
|
|
10634
|
+
} catch {
|
|
10635
|
+
return null;
|
|
10636
|
+
}
|
|
10637
|
+
const port = parsed.port ? Number.parseInt(parsed.port, 10) : parsed.protocol === "https:" ? 443 : parsed.protocol === "http:" ? 80 : Number.NaN;
|
|
10638
|
+
if (!parsed.hostname || !Number.isInteger(port))
|
|
10639
|
+
return null;
|
|
10640
|
+
return { host: parsed.hostname, port };
|
|
10641
|
+
}
|
|
10642
|
+
|
|
10643
|
+
// src/cli/doctor/check-local-gateway.ts
|
|
10644
|
+
var PROBE_TIMEOUT_MS = 1200;
|
|
10645
|
+
function probeTcp(host, port) {
|
|
10646
|
+
return new Promise((resolve3) => {
|
|
10647
|
+
const socket = import_node_net.default.connect({ host, port });
|
|
10648
|
+
let settled = false;
|
|
10649
|
+
const finish = (result) => {
|
|
10650
|
+
if (settled)
|
|
10651
|
+
return;
|
|
10652
|
+
settled = true;
|
|
10653
|
+
socket.destroy();
|
|
10654
|
+
resolve3(result);
|
|
10655
|
+
};
|
|
10656
|
+
socket.setTimeout(PROBE_TIMEOUT_MS);
|
|
10657
|
+
socket.once("connect", () => finish({ ok: true }));
|
|
10658
|
+
socket.once("timeout", () => finish({ ok: false, error: "timeout" }));
|
|
10659
|
+
socket.once("error", (err2) => finish({ ok: false, error: err2.message }));
|
|
10660
|
+
});
|
|
10661
|
+
}
|
|
10662
|
+
var checkLocalGateway = async ({ cfg }) => {
|
|
10663
|
+
const resolution = resolveLocalGatewayBaseUrl({ config: cfg });
|
|
10664
|
+
if (resolution.warning) {
|
|
10665
|
+
return {
|
|
10666
|
+
id: "local-gateway",
|
|
10667
|
+
severity: "warn",
|
|
10668
|
+
title: "本地 Gateway 地址配置无效",
|
|
10669
|
+
detail: `${resolution.warning},当前回退到 ${resolution.baseUrl}`,
|
|
10670
|
+
fixDescription: "请修正 PHONE_NOTIFICATIONS_GATEWAY_BASE_URL,或清理该环境变量后重启 openclaw/gateway",
|
|
10671
|
+
fix: null
|
|
10672
|
+
};
|
|
10673
|
+
}
|
|
10674
|
+
if (resolution.tlsEnabled && !resolution.explicit) {
|
|
10675
|
+
return {
|
|
10676
|
+
id: "local-gateway",
|
|
10677
|
+
severity: "warn",
|
|
10678
|
+
title: "Gateway TLS 可能阻断 Relay 本地代理",
|
|
10679
|
+
detail: `检测到 gateway.tls.enabled=true,但插件本地代理默认连接 ${resolution.baseUrl}。` + "如果 Gateway 只接受 HTTPS/WSS,本地 RPC 握手会失败。",
|
|
10680
|
+
fixDescription: "设置 PHONE_NOTIFICATIONS_GATEWAY_BASE_URL=https://127.0.0.1:<port>,或关闭 gateway.tls.enabled 后重启 gateway",
|
|
10681
|
+
fix: null
|
|
10682
|
+
};
|
|
10683
|
+
}
|
|
10684
|
+
const target = resolveLocalGatewayTcpTarget(resolution.baseUrl);
|
|
10685
|
+
if (!target) {
|
|
10686
|
+
return {
|
|
10687
|
+
id: "local-gateway",
|
|
10688
|
+
severity: "warn",
|
|
10689
|
+
title: "本地 Gateway 地址无法解析",
|
|
10690
|
+
detail: `无法解析插件将使用的本地 Gateway 地址: ${resolution.baseUrl}`,
|
|
10691
|
+
fixDescription: "请设置 PHONE_NOTIFICATIONS_GATEWAY_BASE_URL=http://127.0.0.1:<port>",
|
|
10692
|
+
fix: null
|
|
10693
|
+
};
|
|
10694
|
+
}
|
|
10695
|
+
const probe = await probeTcp(target.host, target.port);
|
|
10696
|
+
if (probe.ok)
|
|
10697
|
+
return null;
|
|
10698
|
+
return {
|
|
10699
|
+
id: "local-gateway",
|
|
10700
|
+
severity: "warn",
|
|
10701
|
+
title: "本地 Gateway 不可达",
|
|
10702
|
+
detail: `插件将连接 ${resolution.baseUrl},但 TCP 探测失败` + (probe.error ? `: ${probe.error}` : ""),
|
|
10703
|
+
fixDescription: "请执行 openclaw gateway status,确认 gateway.port 与 PHONE_NOTIFICATIONS_GATEWAY_BASE_URL 是否一致",
|
|
10704
|
+
fix: null
|
|
10705
|
+
};
|
|
10706
|
+
};
|
|
10707
|
+
|
|
9973
10708
|
// src/cli/doctor/check-node-version.ts
|
|
9974
10709
|
var MIN_MAJOR = 22;
|
|
9975
10710
|
var MIN_MINOR = 12;
|
|
@@ -10029,6 +10764,7 @@ var allCheckers = [
|
|
|
10029
10764
|
checkStateDirPerms,
|
|
10030
10765
|
checkToolPolicy,
|
|
10031
10766
|
checkTunnel,
|
|
10767
|
+
checkLocalGateway,
|
|
10032
10768
|
checkNodeVersion,
|
|
10033
10769
|
checkPluginVersion
|
|
10034
10770
|
];
|
|
@@ -10038,10 +10774,10 @@ function isObject5(v) {
|
|
|
10038
10774
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
10039
10775
|
}
|
|
10040
10776
|
function readConfig(configPath) {
|
|
10041
|
-
if (!
|
|
10777
|
+
if (!import_node_fs23.existsSync(configPath))
|
|
10042
10778
|
return {};
|
|
10043
10779
|
try {
|
|
10044
|
-
const parsed = JSON.parse(
|
|
10780
|
+
const parsed = JSON.parse(import_node_fs23.readFileSync(configPath, "utf-8"));
|
|
10045
10781
|
return isObject5(parsed) ? parsed : {};
|
|
10046
10782
|
} catch {
|
|
10047
10783
|
return {};
|
|
@@ -10252,7 +10988,7 @@ function registerRecStoragePath(rec, ctx) {
|
|
|
10252
10988
|
|
|
10253
10989
|
// src/cli/rec-setup.ts
|
|
10254
10990
|
var import_node_readline2 = require("node:readline");
|
|
10255
|
-
var
|
|
10991
|
+
var import_node_fs24 = require("node:fs");
|
|
10256
10992
|
function ask(rl, question) {
|
|
10257
10993
|
return new Promise((resolve3) => rl.question(question, resolve3));
|
|
10258
10994
|
}
|
|
@@ -10353,9 +11089,9 @@ async function setupLocal(rl) {
|
|
|
10353
11089
|
function registerRecSetup(rec, ctx) {
|
|
10354
11090
|
rec.command("setup").description("交互式配置 ASR 转写参数,保存到本地配置文件").action(async () => {
|
|
10355
11091
|
const configPath = resolveAsrConfigPath(ctx);
|
|
10356
|
-
if (
|
|
11092
|
+
if (import_node_fs24.existsSync(configPath)) {
|
|
10357
11093
|
try {
|
|
10358
|
-
const existing = JSON.parse(
|
|
11094
|
+
const existing = JSON.parse(import_node_fs24.readFileSync(configPath, "utf-8"));
|
|
10359
11095
|
process.stderr.write(`当前已有配置:mode = ${existing.mode}`);
|
|
10360
11096
|
if (existing.updatedAt)
|
|
10361
11097
|
process.stderr.write(`,更新于 ${existing.updatedAt}`);
|
|
@@ -10371,7 +11107,7 @@ function registerRecSetup(rec, ctx) {
|
|
|
10371
11107
|
const modeIdx = await askChoice(rl, "选择模式", ["api(云端 model-proxy 长录音)", "local(本地 Whisper)"]);
|
|
10372
11108
|
const config = modeIdx === 0 ? await setupApi(rl) : await setupLocal(rl);
|
|
10373
11109
|
const stored = { ...config, updatedAt: new Date().toISOString() };
|
|
10374
|
-
|
|
11110
|
+
import_node_fs24.writeFileSync(configPath, JSON.stringify(stored, null, 2), "utf-8");
|
|
10375
11111
|
process.stderr.write(`
|
|
10376
11112
|
✓ 配置已保存到 ${configPath}
|
|
10377
11113
|
|
|
@@ -10467,9 +11203,9 @@ function registerImageStoragePath(image, ctx) {
|
|
|
10467
11203
|
}
|
|
10468
11204
|
|
|
10469
11205
|
// src/cli/update.ts
|
|
10470
|
-
var
|
|
10471
|
-
var
|
|
10472
|
-
var
|
|
11206
|
+
var import_node_child_process5 = require("node:child_process");
|
|
11207
|
+
var import_node_fs25 = require("node:fs");
|
|
11208
|
+
var import_node_path20 = require("node:path");
|
|
10473
11209
|
var import_node_os2 = __toESM(require("node:os"));
|
|
10474
11210
|
async function fetchText(url) {
|
|
10475
11211
|
const res = await fetch(url, { signal: AbortSignal.timeout(30000) });
|
|
@@ -10542,9 +11278,9 @@ async function runUpdate(ctx, opts) {
|
|
|
10542
11278
|
`);
|
|
10543
11279
|
process.exit(1);
|
|
10544
11280
|
}
|
|
10545
|
-
const tmpScript =
|
|
11281
|
+
const tmpScript = import_node_path20.join(import_node_os2.default.tmpdir(), `openclaw-install-${Date.now()}.mjs`);
|
|
10546
11282
|
try {
|
|
10547
|
-
|
|
11283
|
+
import_node_fs25.writeFileSync(tmpScript, installScript, "utf-8");
|
|
10548
11284
|
} catch (err2) {
|
|
10549
11285
|
const msg = `写入临时文件失败: ${err2?.message ?? String(err2)}`;
|
|
10550
11286
|
if (json) {
|
|
@@ -10556,9 +11292,9 @@ async function runUpdate(ctx, opts) {
|
|
|
10556
11292
|
process.exit(1);
|
|
10557
11293
|
}
|
|
10558
11294
|
const stateDir = resolveStateDir(ctx.stateDir);
|
|
10559
|
-
const result =
|
|
11295
|
+
const result = import_node_child_process5.spawnSync(process.execPath, [tmpScript, "--version", latest, "--state-dir", stateDir], { stdio: "inherit" });
|
|
10560
11296
|
try {
|
|
10561
|
-
|
|
11297
|
+
import_node_fs25.unlinkSync(tmpScript);
|
|
10562
11298
|
} catch {}
|
|
10563
11299
|
if (result.error) {
|
|
10564
11300
|
const msg = `安装脚本执行失败: ${result.error.message}`;
|
|
@@ -10632,10 +11368,10 @@ function inferOpenClawRootDir(workspaceDir) {
|
|
|
10632
11368
|
if (!workspaceDir) {
|
|
10633
11369
|
return;
|
|
10634
11370
|
}
|
|
10635
|
-
if (
|
|
11371
|
+
if (import_node_path21.basename(workspaceDir) !== "workspace") {
|
|
10636
11372
|
return;
|
|
10637
11373
|
}
|
|
10638
|
-
return
|
|
11374
|
+
return import_node_path21.dirname(workspaceDir);
|
|
10639
11375
|
}
|
|
10640
11376
|
function registerPluginCli(api, params) {
|
|
10641
11377
|
const { logger, openclawDir } = params;
|
|
@@ -10655,12 +11391,12 @@ function registerPluginCli(api, params) {
|
|
|
10655
11391
|
}
|
|
10656
11392
|
|
|
10657
11393
|
// src/plugin/ntf-bin.ts
|
|
10658
|
-
var
|
|
10659
|
-
var
|
|
11394
|
+
var import_node_fs26 = require("node:fs");
|
|
11395
|
+
var import_node_path22 = require("node:path");
|
|
10660
11396
|
var import_node_process = __toESM(require("node:process"));
|
|
10661
11397
|
function ensureNtfBinInstalled(opts) {
|
|
10662
11398
|
const { ntfCjsPath, stateDir, logger } = opts;
|
|
10663
|
-
if (!
|
|
11399
|
+
if (!import_node_fs26.existsSync(ntfCjsPath)) {
|
|
10664
11400
|
logger.warn(`ntf wrapper: 目标不存在 ${ntfCjsPath},跳过`);
|
|
10665
11401
|
return;
|
|
10666
11402
|
}
|
|
@@ -10688,29 +11424,29 @@ function resolveNtfBinInstallDirs(opts) {
|
|
|
10688
11424
|
const envPath = opts.envPath ?? import_node_process.default.env.PATH ?? "";
|
|
10689
11425
|
const pathDirs = splitPathDirs(envPath);
|
|
10690
11426
|
const candidates = [];
|
|
10691
|
-
pushCandidate(candidates,
|
|
11427
|
+
pushCandidate(candidates, import_node_path22.dirname(import_node_process.default.execPath), "node 同目录", true);
|
|
10692
11428
|
for (const cliName of inferHostCliNames(argv)) {
|
|
10693
11429
|
const cliPath = findCommandOnPath(cliName, pathDirs);
|
|
10694
11430
|
if (cliPath) {
|
|
10695
|
-
pushCandidate(candidates,
|
|
11431
|
+
pushCandidate(candidates, import_node_path22.dirname(cliPath), `宿主 CLI (${cliName}) 同目录`, true);
|
|
10696
11432
|
}
|
|
10697
11433
|
}
|
|
10698
11434
|
for (const dir of pathDirs) {
|
|
10699
11435
|
pushCandidate(candidates, dir, "PATH 目录", true);
|
|
10700
11436
|
}
|
|
10701
11437
|
if (opts.stateDir) {
|
|
10702
|
-
pushCandidate(candidates,
|
|
11438
|
+
pushCandidate(candidates, import_node_path22.join(opts.stateDir, "bin"), "stateDir/bin 兜底", false);
|
|
10703
11439
|
}
|
|
10704
11440
|
return candidates;
|
|
10705
11441
|
}
|
|
10706
11442
|
function tryInstallWrapper(dir, target) {
|
|
10707
11443
|
try {
|
|
10708
|
-
|
|
11444
|
+
import_node_fs26.mkdirSync(dir, { recursive: true });
|
|
10709
11445
|
} catch (err2) {
|
|
10710
11446
|
return { ok: false, reason: `mkdir ${dir} 失败: ${describeError(err2)}` };
|
|
10711
11447
|
}
|
|
10712
11448
|
const wrapperName = import_node_process.default.platform === "win32" ? "ntf.cmd" : "ntf";
|
|
10713
|
-
const wrapperPath =
|
|
11449
|
+
const wrapperPath = import_node_path22.join(dir, wrapperName);
|
|
10714
11450
|
if (pathExists(wrapperPath)) {
|
|
10715
11451
|
if (isExistingWrapperForTarget(wrapperPath, target)) {
|
|
10716
11452
|
return { ok: true, path: wrapperPath, created: false };
|
|
@@ -10719,13 +11455,13 @@ function tryInstallWrapper(dir, target) {
|
|
|
10719
11455
|
}
|
|
10720
11456
|
try {
|
|
10721
11457
|
if (import_node_process.default.platform === "win32") {
|
|
10722
|
-
|
|
11458
|
+
import_node_fs26.writeFileSync(wrapperPath, `@echo off\r
|
|
10723
11459
|
node "${target}" %*\r
|
|
10724
11460
|
`, "utf-8");
|
|
10725
11461
|
} else {
|
|
10726
|
-
|
|
11462
|
+
import_node_fs26.symlinkSync(target, wrapperPath);
|
|
10727
11463
|
try {
|
|
10728
|
-
|
|
11464
|
+
import_node_fs26.chmodSync(target, 493);
|
|
10729
11465
|
} catch {}
|
|
10730
11466
|
}
|
|
10731
11467
|
return { ok: true, path: wrapperPath, created: true };
|
|
@@ -10735,7 +11471,7 @@ node "${target}" %*\r
|
|
|
10735
11471
|
}
|
|
10736
11472
|
function pathExists(path) {
|
|
10737
11473
|
try {
|
|
10738
|
-
|
|
11474
|
+
import_node_fs26.lstatSync(path);
|
|
10739
11475
|
return true;
|
|
10740
11476
|
} catch {
|
|
10741
11477
|
return false;
|
|
@@ -10744,16 +11480,16 @@ function pathExists(path) {
|
|
|
10744
11480
|
function isExistingWrapperForTarget(wrapperPath, target) {
|
|
10745
11481
|
if (import_node_process.default.platform === "win32") {
|
|
10746
11482
|
try {
|
|
10747
|
-
return
|
|
11483
|
+
return import_node_fs26.readFileSync(wrapperPath, "utf-8").includes(`"${target}"`);
|
|
10748
11484
|
} catch {
|
|
10749
11485
|
return false;
|
|
10750
11486
|
}
|
|
10751
11487
|
}
|
|
10752
11488
|
try {
|
|
10753
|
-
if (!
|
|
11489
|
+
if (!import_node_fs26.lstatSync(wrapperPath).isSymbolicLink()) {
|
|
10754
11490
|
return false;
|
|
10755
11491
|
}
|
|
10756
|
-
return
|
|
11492
|
+
return import_node_fs26.realpathSync(wrapperPath) === import_node_fs26.realpathSync(target);
|
|
10757
11493
|
} catch {
|
|
10758
11494
|
return false;
|
|
10759
11495
|
}
|
|
@@ -10764,7 +11500,7 @@ function describeError(err2) {
|
|
|
10764
11500
|
return String(err2);
|
|
10765
11501
|
}
|
|
10766
11502
|
function splitPathDirs(envPath) {
|
|
10767
|
-
return envPath.split(
|
|
11503
|
+
return envPath.split(import_node_path22.delimiter).map((entry) => entry.trim()).filter((entry) => entry && import_node_path22.isAbsolute(entry));
|
|
10768
11504
|
}
|
|
10769
11505
|
function inferHostCliNames(argv) {
|
|
10770
11506
|
const names = ["openclaw", "jvsclaw", "qclaw"];
|
|
@@ -10778,8 +11514,8 @@ function findCommandOnPath(command, pathDirs) {
|
|
|
10778
11514
|
const names = import_node_process.default.platform === "win32" && !command.toLowerCase().endsWith(".cmd") ? [command, `${command}.cmd`, `${command}.exe`] : [command];
|
|
10779
11515
|
for (const dir of pathDirs) {
|
|
10780
11516
|
for (const name of names) {
|
|
10781
|
-
const candidate =
|
|
10782
|
-
if (
|
|
11517
|
+
const candidate = import_node_path22.join(dir, name);
|
|
11518
|
+
if (import_node_fs26.existsSync(candidate)) {
|
|
10783
11519
|
return candidate;
|
|
10784
11520
|
}
|
|
10785
11521
|
}
|
|
@@ -10794,11 +11530,11 @@ function pushCandidate(candidates, dir, label, onPath) {
|
|
|
10794
11530
|
}
|
|
10795
11531
|
|
|
10796
11532
|
// src/plugin/lifecycle.ts
|
|
10797
|
-
var
|
|
11533
|
+
var import_node_fs36 = require("node:fs");
|
|
10798
11534
|
|
|
10799
11535
|
// src/notification/app-name-map.ts
|
|
10800
|
-
var
|
|
10801
|
-
var
|
|
11536
|
+
var import_node_fs27 = require("node:fs");
|
|
11537
|
+
var import_node_path23 = require("node:path");
|
|
10802
11538
|
var PLUGIN_STATE_DIR = "phone-notifications";
|
|
10803
11539
|
var CACHE_FILE = "app-name-map.json";
|
|
10804
11540
|
var BUILTIN_APP_NAME_MAP_URL = getEnvUrls().appNameMapUrl;
|
|
@@ -10819,7 +11555,7 @@ function isAppNameMapApiResponse(v) {
|
|
|
10819
11555
|
return Array.isArray(o.data) && o.data.every((item) => item !== null && typeof item === "object" && typeof item.packageName === "string" && typeof item.appName === "string");
|
|
10820
11556
|
}
|
|
10821
11557
|
function getCachePath(stateDir) {
|
|
10822
|
-
return
|
|
11558
|
+
return import_node_path23.join(stateDir, "plugins", PLUGIN_STATE_DIR, CACHE_FILE);
|
|
10823
11559
|
}
|
|
10824
11560
|
function createAppNameMapProvider(opts) {
|
|
10825
11561
|
const { stateDir, logger } = opts;
|
|
@@ -10831,10 +11567,10 @@ function createAppNameMapProvider(opts) {
|
|
|
10831
11567
|
let inFlightFetch = null;
|
|
10832
11568
|
function loadFromDisk() {
|
|
10833
11569
|
const path = getCachePath(stateDir);
|
|
10834
|
-
if (!
|
|
11570
|
+
if (!import_node_fs27.existsSync(path))
|
|
10835
11571
|
return;
|
|
10836
11572
|
try {
|
|
10837
|
-
const raw = JSON.parse(
|
|
11573
|
+
const raw = JSON.parse(import_node_fs27.readFileSync(path, "utf-8"));
|
|
10838
11574
|
if (!isRecordOfStrings(raw))
|
|
10839
11575
|
return;
|
|
10840
11576
|
map.clear();
|
|
@@ -10880,10 +11616,10 @@ function createAppNameMapProvider(opts) {
|
|
|
10880
11616
|
logger.warn("[app-name-map] refresh succeeded but got 0 entries");
|
|
10881
11617
|
return;
|
|
10882
11618
|
}
|
|
10883
|
-
const dir =
|
|
10884
|
-
|
|
11619
|
+
const dir = import_node_path23.join(stateDir, "plugins", PLUGIN_STATE_DIR);
|
|
11620
|
+
import_node_fs27.mkdirSync(dir, { recursive: true });
|
|
10885
11621
|
const cachePath = getCachePath(stateDir);
|
|
10886
|
-
|
|
11622
|
+
import_node_fs27.writeFileSync(cachePath, JSON.stringify(Object.fromEntries(map), null, 2), "utf-8");
|
|
10887
11623
|
logger.info(`[app-name-map] refreshed ${map.size} entries from server and saved: ${cachePath}`);
|
|
10888
11624
|
} catch (e) {
|
|
10889
11625
|
const message = e instanceof Error ? e.message : String(e);
|
|
@@ -10934,8 +11670,8 @@ function createAppNameMapProvider(opts) {
|
|
|
10934
11670
|
}
|
|
10935
11671
|
|
|
10936
11672
|
// src/recording/storage.ts
|
|
10937
|
-
var
|
|
10938
|
-
var
|
|
11673
|
+
var import_node_fs28 = require("node:fs");
|
|
11674
|
+
var import_node_path24 = require("node:path");
|
|
10939
11675
|
|
|
10940
11676
|
// src/recording/state-machine.ts
|
|
10941
11677
|
var VALID_TRANSITIONS = new Map([
|
|
@@ -11199,7 +11935,7 @@ function stripMarkdownFence(markdown) {
|
|
|
11199
11935
|
function deriveTitleFromTranscriptPath(transcriptFile, recordingId) {
|
|
11200
11936
|
if (!transcriptFile)
|
|
11201
11937
|
return;
|
|
11202
|
-
const name =
|
|
11938
|
+
const name = import_node_path24.basename(transcriptFile, ".md");
|
|
11203
11939
|
const prefix = `${recordingId}_`;
|
|
11204
11940
|
if (name.startsWith(prefix)) {
|
|
11205
11941
|
const derived = name.slice(prefix.length).trim();
|
|
@@ -11241,16 +11977,16 @@ function extractTranscriptContent(markdown) {
|
|
|
11241
11977
|
`).trim();
|
|
11242
11978
|
}
|
|
11243
11979
|
function resolveRecordingStorageDir(ctx, logger) {
|
|
11244
|
-
const stateRecDir =
|
|
11980
|
+
const stateRecDir = import_node_path24.join(ctx.stateDir, "plugins", "phone-notifications", RECORDINGS_DIR);
|
|
11245
11981
|
try {
|
|
11246
|
-
|
|
11982
|
+
import_node_fs28.mkdirSync(stateRecDir, { recursive: true });
|
|
11247
11983
|
logger.info(`录音将写入 stateDir 路径: ${stateRecDir}`);
|
|
11248
11984
|
return stateRecDir;
|
|
11249
11985
|
} catch {}
|
|
11250
11986
|
if (ctx.workspaceDir) {
|
|
11251
|
-
const wsRecDir =
|
|
11987
|
+
const wsRecDir = import_node_path24.join(ctx.workspaceDir, RECORDINGS_DIR);
|
|
11252
11988
|
try {
|
|
11253
|
-
|
|
11989
|
+
import_node_fs28.mkdirSync(wsRecDir, { recursive: true });
|
|
11254
11990
|
logger.warn(`stateDir 不可用,录音已回退到 workspace 路径: ${wsRecDir}`);
|
|
11255
11991
|
return wsRecDir;
|
|
11256
11992
|
} catch {}
|
|
@@ -11270,17 +12006,17 @@ class RecordingStorage {
|
|
|
11270
12006
|
constructor(dir, logger) {
|
|
11271
12007
|
this.logger = logger;
|
|
11272
12008
|
this.dir = dir;
|
|
11273
|
-
this.audioDir =
|
|
11274
|
-
this.transcriptDataDir =
|
|
11275
|
-
this.transcriptsDir =
|
|
11276
|
-
this.summariesDir =
|
|
11277
|
-
this.indexPath =
|
|
12009
|
+
this.audioDir = import_node_path24.join(dir, AUDIO_DIR);
|
|
12010
|
+
this.transcriptDataDir = import_node_path24.join(dir, TRANSCRIPT_DATA_DIR);
|
|
12011
|
+
this.transcriptsDir = import_node_path24.join(dir, TRANSCRIPTS_DIR);
|
|
12012
|
+
this.summariesDir = import_node_path24.join(dir, SUMMARIES_DIR2);
|
|
12013
|
+
this.indexPath = import_node_path24.join(dir, INDEX_FILE);
|
|
11278
12014
|
}
|
|
11279
12015
|
async init() {
|
|
11280
|
-
|
|
11281
|
-
|
|
11282
|
-
|
|
11283
|
-
|
|
12016
|
+
import_node_fs28.mkdirSync(this.audioDir, { recursive: true });
|
|
12017
|
+
import_node_fs28.mkdirSync(this.transcriptDataDir, { recursive: true });
|
|
12018
|
+
import_node_fs28.mkdirSync(this.transcriptsDir, { recursive: true });
|
|
12019
|
+
import_node_fs28.mkdirSync(this.summariesDir, { recursive: true });
|
|
11284
12020
|
this.loadIndex();
|
|
11285
12021
|
this.logger.info(`录音存储已初始化: ${this.dir}(共 ${this.index.recordings.length} 条记录)`);
|
|
11286
12022
|
}
|
|
@@ -11312,13 +12048,13 @@ class RecordingStorage {
|
|
|
11312
12048
|
return id;
|
|
11313
12049
|
}
|
|
11314
12050
|
if (existing.transcriptDataFile) {
|
|
11315
|
-
|
|
12051
|
+
import_node_fs28.rmSync(import_node_path24.join(this.dir, existing.transcriptDataFile), { force: true });
|
|
11316
12052
|
}
|
|
11317
12053
|
if (existing.transcriptFile) {
|
|
11318
|
-
|
|
12054
|
+
import_node_fs28.rmSync(import_node_path24.join(this.dir, existing.transcriptFile), { force: true });
|
|
11319
12055
|
}
|
|
11320
12056
|
if (existing.summaryFile) {
|
|
11321
|
-
|
|
12057
|
+
import_node_fs28.rmSync(import_node_path24.join(this.dir, existing.summaryFile), { force: true });
|
|
11322
12058
|
}
|
|
11323
12059
|
existing.metadata = metadata;
|
|
11324
12060
|
existing.clientLabel = clientLabel;
|
|
@@ -11375,7 +12111,7 @@ class RecordingStorage {
|
|
|
11375
12111
|
return;
|
|
11376
12112
|
const nextAudioFile = `${AUDIO_DIR}/${filename}`;
|
|
11377
12113
|
if (entry.audioFile && entry.audioFile !== nextAudioFile) {
|
|
11378
|
-
|
|
12114
|
+
import_node_fs28.rmSync(import_node_path24.join(this.dir, entry.audioFile), { force: true });
|
|
11379
12115
|
}
|
|
11380
12116
|
entry.audioFile = nextAudioFile;
|
|
11381
12117
|
entry.updatedAt = new Date().toISOString();
|
|
@@ -11395,7 +12131,7 @@ class RecordingStorage {
|
|
|
11395
12131
|
return;
|
|
11396
12132
|
const nextTranscriptDataFile = `${TRANSCRIPT_DATA_DIR}/${filename}`;
|
|
11397
12133
|
if (entry.transcriptDataFile && entry.transcriptDataFile !== nextTranscriptDataFile) {
|
|
11398
|
-
|
|
12134
|
+
import_node_fs28.rmSync(import_node_path24.join(this.dir, entry.transcriptDataFile), { force: true });
|
|
11399
12135
|
}
|
|
11400
12136
|
entry.transcriptDataFile = nextTranscriptDataFile;
|
|
11401
12137
|
entry.updatedAt = new Date().toISOString();
|
|
@@ -11407,7 +12143,7 @@ class RecordingStorage {
|
|
|
11407
12143
|
return;
|
|
11408
12144
|
const nextTranscriptFile = `${TRANSCRIPTS_DIR}/${filename}`;
|
|
11409
12145
|
if (entry.transcriptFile && entry.transcriptFile !== nextTranscriptFile) {
|
|
11410
|
-
|
|
12146
|
+
import_node_fs28.rmSync(import_node_path24.join(this.dir, entry.transcriptFile), { force: true });
|
|
11411
12147
|
}
|
|
11412
12148
|
entry.transcriptFile = nextTranscriptFile;
|
|
11413
12149
|
entry.updatedAt = new Date().toISOString();
|
|
@@ -11419,7 +12155,7 @@ class RecordingStorage {
|
|
|
11419
12155
|
return;
|
|
11420
12156
|
const nextSummaryFile = `${SUMMARIES_DIR2}/${filename}`;
|
|
11421
12157
|
if (entry.summaryFile && entry.summaryFile !== nextSummaryFile) {
|
|
11422
|
-
|
|
12158
|
+
import_node_fs28.rmSync(import_node_path24.join(this.dir, entry.summaryFile), { force: true });
|
|
11423
12159
|
}
|
|
11424
12160
|
entry.summaryFile = nextSummaryFile;
|
|
11425
12161
|
entry.updatedAt = new Date().toISOString();
|
|
@@ -11508,24 +12244,24 @@ class RecordingStorage {
|
|
|
11508
12244
|
if (!entry)
|
|
11509
12245
|
return false;
|
|
11510
12246
|
if (entry.audioFile) {
|
|
11511
|
-
const audioPath =
|
|
11512
|
-
|
|
12247
|
+
const audioPath = import_node_path24.join(this.dir, entry.audioFile);
|
|
12248
|
+
import_node_fs28.rmSync(audioPath, { force: true });
|
|
11513
12249
|
}
|
|
11514
12250
|
if (entry.srtFile) {
|
|
11515
|
-
const srtPath =
|
|
11516
|
-
|
|
12251
|
+
const srtPath = import_node_path24.join(this.dir, entry.srtFile);
|
|
12252
|
+
import_node_fs28.rmSync(srtPath, { force: true });
|
|
11517
12253
|
}
|
|
11518
12254
|
if (entry.transcriptDataFile) {
|
|
11519
|
-
const transcriptDataPath =
|
|
11520
|
-
|
|
12255
|
+
const transcriptDataPath = import_node_path24.join(this.dir, entry.transcriptDataFile);
|
|
12256
|
+
import_node_fs28.rmSync(transcriptDataPath, { force: true });
|
|
11521
12257
|
}
|
|
11522
12258
|
if (entry.transcriptFile) {
|
|
11523
|
-
const transcriptPath =
|
|
11524
|
-
|
|
12259
|
+
const transcriptPath = import_node_path24.join(this.dir, entry.transcriptFile);
|
|
12260
|
+
import_node_fs28.rmSync(transcriptPath, { force: true });
|
|
11525
12261
|
}
|
|
11526
12262
|
if (entry.summaryFile) {
|
|
11527
|
-
const summaryPath =
|
|
11528
|
-
|
|
12263
|
+
const summaryPath = import_node_path24.join(this.dir, entry.summaryFile);
|
|
12264
|
+
import_node_fs28.rmSync(summaryPath, { force: true });
|
|
11529
12265
|
}
|
|
11530
12266
|
if (opts?.localOnly) {
|
|
11531
12267
|
entry.audioFile = undefined;
|
|
@@ -11561,24 +12297,24 @@ class RecordingStorage {
|
|
|
11561
12297
|
return `${recordingId}.md`;
|
|
11562
12298
|
}
|
|
11563
12299
|
getAudioFilePath(recordingId, ossUrl) {
|
|
11564
|
-
return
|
|
12300
|
+
return import_node_path24.join(this.audioDir, this.buildAudioFilename(recordingId, ossUrl));
|
|
11565
12301
|
}
|
|
11566
12302
|
getSrtFilePath(recordingId) {
|
|
11567
|
-
return
|
|
12303
|
+
return import_node_path24.join(this.audioDir, this.buildSrtFilename(recordingId));
|
|
11568
12304
|
}
|
|
11569
12305
|
getTranscriptDataFilePath(recordingId) {
|
|
11570
|
-
return
|
|
12306
|
+
return import_node_path24.join(this.transcriptDataDir, this.buildTranscriptDataFilename(recordingId));
|
|
11571
12307
|
}
|
|
11572
12308
|
getSummaryFilePath(recordingId) {
|
|
11573
|
-
return
|
|
12309
|
+
return import_node_path24.join(this.summariesDir, this.buildSummaryFilename(recordingId));
|
|
11574
12310
|
}
|
|
11575
12311
|
loadIndex() {
|
|
11576
|
-
if (!
|
|
12312
|
+
if (!import_node_fs28.existsSync(this.indexPath)) {
|
|
11577
12313
|
this.index = { recordings: [] };
|
|
11578
12314
|
return;
|
|
11579
12315
|
}
|
|
11580
12316
|
try {
|
|
11581
|
-
const raw = JSON.parse(
|
|
12317
|
+
const raw = JSON.parse(import_node_fs28.readFileSync(this.indexPath, "utf-8"));
|
|
11582
12318
|
if (raw && Array.isArray(raw.recordings)) {
|
|
11583
12319
|
let needsRewrite = false;
|
|
11584
12320
|
const normalized = raw.recordings.filter((entry) => entry && typeof entry === "object").map((entry) => {
|
|
@@ -11599,14 +12335,23 @@ class RecordingStorage {
|
|
|
11599
12335
|
if (typeof entry.transcriptFile === "string") {
|
|
11600
12336
|
compacted.transcriptFile = entry.transcriptFile;
|
|
11601
12337
|
}
|
|
11602
|
-
let
|
|
11603
|
-
|
|
12338
|
+
let transcriptDocLoaded = false;
|
|
12339
|
+
let transcriptDoc;
|
|
12340
|
+
const loadTranscriptDoc = () => {
|
|
12341
|
+
if (!transcriptDocLoaded) {
|
|
12342
|
+
transcriptDocLoaded = true;
|
|
12343
|
+
transcriptDoc = typeof compacted.transcriptDataFile === "string" ? this.readRelativeTranscriptDocument(compacted.transcriptDataFile) : undefined;
|
|
12344
|
+
}
|
|
12345
|
+
return transcriptDoc;
|
|
12346
|
+
};
|
|
12347
|
+
if (typeof entry.transcriptDataFile === "string") {
|
|
11604
12348
|
compacted.transcriptDataFile = entry.transcriptDataFile;
|
|
11605
12349
|
} else {
|
|
11606
12350
|
const legacyTranscriptText = typeof entry.transcript === "string" && entry.transcript.trim() ? entry.transcript.trim() : compacted.transcriptFile ? extractTranscriptContent(this.readRelativeTextFile(compacted.transcriptFile) ?? "") || undefined : undefined;
|
|
11607
12351
|
const legacyTitle = typeof entry.title === "string" && entry.title.trim() ? entry.title.trim() : deriveTitleFromTranscriptPath(compacted.transcriptFile, compacted.id);
|
|
11608
12352
|
const legacySummary = typeof entry.summary === "string" && entry.summary.trim() ? entry.summary.trim() : undefined;
|
|
11609
12353
|
if (legacyTranscriptText) {
|
|
12354
|
+
transcriptDocLoaded = true;
|
|
11610
12355
|
transcriptDoc = buildTranscriptDocument({
|
|
11611
12356
|
recordingId: compacted.id,
|
|
11612
12357
|
generatedAt: compacted.updatedAt,
|
|
@@ -11619,7 +12364,7 @@ class RecordingStorage {
|
|
|
11619
12364
|
segments: []
|
|
11620
12365
|
});
|
|
11621
12366
|
const transcriptDataFilename = this.buildTranscriptDataFilename(compacted.id);
|
|
11622
|
-
|
|
12367
|
+
import_node_fs28.writeFileSync(import_node_path24.join(this.transcriptDataDir, transcriptDataFilename), JSON.stringify(transcriptDoc, null, 2), "utf-8");
|
|
11623
12368
|
compacted.transcriptDataFile = `${TRANSCRIPT_DATA_DIR}/${transcriptDataFilename}`;
|
|
11624
12369
|
needsRewrite = true;
|
|
11625
12370
|
}
|
|
@@ -11628,14 +12373,14 @@ class RecordingStorage {
|
|
|
11628
12373
|
compacted.summaryFile = entry.summaryFile;
|
|
11629
12374
|
} else if (typeof entry.summary === "string" && entry.summary.trim()) {
|
|
11630
12375
|
const summaryFilename = this.buildSummaryFilename(entry.id);
|
|
11631
|
-
|
|
12376
|
+
import_node_fs28.writeFileSync(import_node_path24.join(this.summariesDir, summaryFilename), entry.summary.trim(), "utf-8");
|
|
11632
12377
|
compacted.summaryFile = `${SUMMARIES_DIR2}/${summaryFilename}`;
|
|
11633
12378
|
needsRewrite = true;
|
|
11634
12379
|
} else {
|
|
11635
|
-
const summaryFromDocument = extractTranscriptSummaryFromDocument(
|
|
12380
|
+
const summaryFromDocument = extractTranscriptSummaryFromDocument(loadTranscriptDoc());
|
|
11636
12381
|
if (summaryFromDocument) {
|
|
11637
12382
|
const summaryFilename = this.buildSummaryFilename(entry.id);
|
|
11638
|
-
|
|
12383
|
+
import_node_fs28.writeFileSync(import_node_path24.join(this.summariesDir, summaryFilename), summaryFromDocument, "utf-8");
|
|
11639
12384
|
compacted.summaryFile = `${SUMMARIES_DIR2}/${summaryFilename}`;
|
|
11640
12385
|
needsRewrite = true;
|
|
11641
12386
|
}
|
|
@@ -11643,7 +12388,7 @@ class RecordingStorage {
|
|
|
11643
12388
|
if (typeof entry.title === "string" && entry.title.trim()) {
|
|
11644
12389
|
compacted.title = entry.title.trim();
|
|
11645
12390
|
} else {
|
|
11646
|
-
const titleFromDocument = extractTranscriptTitleFromDocument(
|
|
12391
|
+
const titleFromDocument = extractTranscriptTitleFromDocument(loadTranscriptDoc());
|
|
11647
12392
|
const derivedTitle = titleFromDocument ?? deriveTitleFromTranscriptPath(compacted.transcriptFile, compacted.id);
|
|
11648
12393
|
if (derivedTitle) {
|
|
11649
12394
|
compacted.title = derivedTitle;
|
|
@@ -11670,13 +12415,14 @@ class RecordingStorage {
|
|
|
11670
12415
|
this.index = { recordings: [] };
|
|
11671
12416
|
}
|
|
11672
12417
|
} catch {
|
|
11673
|
-
|
|
12418
|
+
const quarantinePath = quarantineCorruptFile(this.indexPath);
|
|
12419
|
+
this.logger.warn(quarantinePath ? `录音索引文件损坏,已隔离到 ${quarantinePath} 并重建空索引` : `录音索引文件损坏且隔离失败,已重建空索引: ${this.indexPath}`);
|
|
11674
12420
|
this.index = { recordings: [] };
|
|
11675
12421
|
}
|
|
11676
12422
|
}
|
|
11677
12423
|
readRelativeTextFile(relativePath) {
|
|
11678
12424
|
try {
|
|
11679
|
-
return
|
|
12425
|
+
return import_node_fs28.readFileSync(import_node_path24.join(this.dir, relativePath), "utf-8");
|
|
11680
12426
|
} catch {
|
|
11681
12427
|
return;
|
|
11682
12428
|
}
|
|
@@ -11689,13 +12435,13 @@ class RecordingStorage {
|
|
|
11689
12435
|
return parseTranscriptDocument(raw);
|
|
11690
12436
|
}
|
|
11691
12437
|
saveIndex() {
|
|
11692
|
-
|
|
12438
|
+
writeFileAtomic(this.indexPath, JSON.stringify(this.index, null, 2));
|
|
11693
12439
|
}
|
|
11694
12440
|
async close() {}
|
|
11695
12441
|
}
|
|
11696
12442
|
// src/recording/downloader.ts
|
|
11697
|
-
var
|
|
11698
|
-
var
|
|
12443
|
+
var import_node_fs29 = require("node:fs");
|
|
12444
|
+
var import_node_path25 = require("node:path");
|
|
11699
12445
|
var import_promises2 = require("node:stream/promises");
|
|
11700
12446
|
var import_node_stream = require("node:stream");
|
|
11701
12447
|
var DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
@@ -11705,7 +12451,7 @@ async function downloadFile(url, destPath, logger, options) {
|
|
|
11705
12451
|
const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
11706
12452
|
const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
11707
12453
|
const retryBackoffMs = options?.retryBackoffMs ?? DEFAULT_RETRY_BACKOFF_MS;
|
|
11708
|
-
|
|
12454
|
+
import_node_fs29.mkdirSync(import_node_path25.dirname(destPath), { recursive: true });
|
|
11709
12455
|
let lastError;
|
|
11710
12456
|
for (let attempt = 1;attempt <= maxRetries; attempt++) {
|
|
11711
12457
|
const startMs = Date.now();
|
|
@@ -11721,11 +12467,11 @@ async function downloadFile(url, destPath, logger, options) {
|
|
|
11721
12467
|
if (!res.body) {
|
|
11722
12468
|
throw new Error("响应体为空");
|
|
11723
12469
|
}
|
|
11724
|
-
const writeStream =
|
|
12470
|
+
const writeStream = import_node_fs29.createWriteStream(destPath);
|
|
11725
12471
|
const readable = import_node_stream.Readable.fromWeb(res.body);
|
|
11726
12472
|
await import_promises2.pipeline(readable, writeStream);
|
|
11727
12473
|
const elapsed = Date.now() - startMs;
|
|
11728
|
-
const fileSize =
|
|
12474
|
+
const fileSize = import_node_fs29.existsSync(destPath) ? import_node_fs29.statSync(destPath).size : 0;
|
|
11729
12475
|
logger.info(`[downloader] 下载完成: ${destPath} (${formatBytes(fileSize)}, ${elapsed}ms)`);
|
|
11730
12476
|
return { ok: true, sizeBytes: fileSize, elapsedMs: elapsed };
|
|
11731
12477
|
} finally {
|
|
@@ -11734,8 +12480,8 @@ async function downloadFile(url, destPath, logger, options) {
|
|
|
11734
12480
|
} catch (err2) {
|
|
11735
12481
|
lastError = err2?.message ?? String(err2);
|
|
11736
12482
|
try {
|
|
11737
|
-
if (
|
|
11738
|
-
|
|
12483
|
+
if (import_node_fs29.existsSync(destPath))
|
|
12484
|
+
import_node_fs29.unlinkSync(destPath);
|
|
11739
12485
|
} catch {}
|
|
11740
12486
|
const isAbort = err2?.name === "AbortError";
|
|
11741
12487
|
logger.warn(`[downloader] 下载失败 (attempt ${attempt}/${maxRetries}): ${isAbort ? "超时" : lastError}`);
|
|
@@ -11759,8 +12505,8 @@ function sleep(ms) {
|
|
|
11759
12505
|
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
11760
12506
|
}
|
|
11761
12507
|
// src/recording/result-writer.ts
|
|
11762
|
-
var
|
|
11763
|
-
var
|
|
12508
|
+
var import_node_fs30 = require("node:fs");
|
|
12509
|
+
var import_node_path26 = require("node:path");
|
|
11764
12510
|
function handleRecordingResultWrite(params, storage2, logger, options = {}) {
|
|
11765
12511
|
const recordingId = normalizeRequiredText(params.recordingId, "recordingId");
|
|
11766
12512
|
if (!params.transcript && !params.summary) {
|
|
@@ -11840,11 +12586,11 @@ function writeTranscript(recordingId, recording, transcript, storage2, logger) {
|
|
|
11840
12586
|
raw: transcript
|
|
11841
12587
|
});
|
|
11842
12588
|
const transcriptDataFilename = storage2.buildTranscriptDataFilename(recordingId);
|
|
11843
|
-
|
|
12589
|
+
import_node_fs30.writeFileSync(storage2.getTranscriptDataFilePath(recordingId), JSON.stringify(transcriptDocument, null, 2), "utf-8");
|
|
11844
12590
|
storage2.setTranscriptDataFile(recordingId, transcriptDataFilename);
|
|
11845
12591
|
const transcriptFilename = storage2.buildTranscriptFilename(recordingId, title);
|
|
11846
12592
|
const markdown = normalizePossiblyEmptyText2(transcript.markdown) ?? buildTranscriptMarkdown(recording, title, text, segments);
|
|
11847
|
-
|
|
12593
|
+
import_node_fs30.writeFileSync(import_node_path26.join(storage2.getTranscriptsDir(), transcriptFilename), ensureTrailingNewline(markdown), "utf-8");
|
|
11848
12594
|
storage2.setTranscriptFile(recordingId, transcriptFilename);
|
|
11849
12595
|
storage2.setTitle(recordingId, title);
|
|
11850
12596
|
logger.info(`[recording-result] 转录文本已写入: ${recordingId}`);
|
|
@@ -11859,7 +12605,7 @@ function writeSummary(recordingId, summary, storage2, logger) {
|
|
|
11859
12605
|
return;
|
|
11860
12606
|
}
|
|
11861
12607
|
const summaryFilename = storage2.buildSummaryFilename(recordingId);
|
|
11862
|
-
|
|
12608
|
+
import_node_fs30.writeFileSync(storage2.getSummaryFilePath(recordingId), ensureTrailingNewline(markdown), "utf-8");
|
|
11863
12609
|
storage2.setSummaryFile(recordingId, summaryFilename);
|
|
11864
12610
|
logger.info(`[recording-result] 总结已写入: ${recordingId}`);
|
|
11865
12611
|
return markdown.trim();
|
|
@@ -12032,8 +12778,8 @@ function formatTimestamp(ms) {
|
|
|
12032
12778
|
return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
|
|
12033
12779
|
}
|
|
12034
12780
|
// src/image/store.ts
|
|
12035
|
-
var
|
|
12036
|
-
var
|
|
12781
|
+
var import_node_fs31 = require("node:fs");
|
|
12782
|
+
var import_node_path27 = require("node:path");
|
|
12037
12783
|
var IMAGES_DIR = "images";
|
|
12038
12784
|
var FILES_DIR = "files";
|
|
12039
12785
|
var INDEX_FILE2 = "index.json";
|
|
@@ -12048,16 +12794,16 @@ function extFromMime(mime) {
|
|
|
12048
12794
|
return "jpg";
|
|
12049
12795
|
}
|
|
12050
12796
|
function resolveImageStorageDir(ctx, logger) {
|
|
12051
|
-
const stateImagesDir =
|
|
12797
|
+
const stateImagesDir = import_node_path27.join(ctx.stateDir, "plugins", "phone-notifications", IMAGES_DIR);
|
|
12052
12798
|
try {
|
|
12053
|
-
|
|
12799
|
+
import_node_fs31.mkdirSync(stateImagesDir, { recursive: true });
|
|
12054
12800
|
logger.info(`图片将写入 stateDir 路径: ${stateImagesDir}`);
|
|
12055
12801
|
return stateImagesDir;
|
|
12056
12802
|
} catch {}
|
|
12057
12803
|
if (ctx.workspaceDir) {
|
|
12058
|
-
const wsImagesDir =
|
|
12804
|
+
const wsImagesDir = import_node_path27.join(ctx.workspaceDir, IMAGES_DIR);
|
|
12059
12805
|
try {
|
|
12060
|
-
|
|
12806
|
+
import_node_fs31.mkdirSync(wsImagesDir, { recursive: true });
|
|
12061
12807
|
logger.warn(`stateDir 不可用,图片已回退到 workspace 路径: ${wsImagesDir}`);
|
|
12062
12808
|
return wsImagesDir;
|
|
12063
12809
|
} catch {}
|
|
@@ -12074,11 +12820,11 @@ class ImageStorage {
|
|
|
12074
12820
|
constructor(dir, logger) {
|
|
12075
12821
|
this.logger = logger;
|
|
12076
12822
|
this.dir = dir;
|
|
12077
|
-
this.filesDir =
|
|
12078
|
-
this.indexPath =
|
|
12823
|
+
this.filesDir = import_node_path27.join(dir, FILES_DIR);
|
|
12824
|
+
this.indexPath = import_node_path27.join(dir, INDEX_FILE2);
|
|
12079
12825
|
}
|
|
12080
12826
|
async init() {
|
|
12081
|
-
|
|
12827
|
+
import_node_fs31.mkdirSync(this.filesDir, { recursive: true });
|
|
12082
12828
|
this.loadIndex();
|
|
12083
12829
|
this.logger.info(`图片存储已初始化: ${this.dir}(共 ${this.index.images.length} 条记录)`);
|
|
12084
12830
|
}
|
|
@@ -12096,7 +12842,7 @@ class ImageStorage {
|
|
|
12096
12842
|
return existing;
|
|
12097
12843
|
}
|
|
12098
12844
|
if (existing.localFile) {
|
|
12099
|
-
|
|
12845
|
+
import_node_fs31.rmSync(import_node_path27.join(this.dir, existing.localFile), { force: true });
|
|
12100
12846
|
}
|
|
12101
12847
|
existing.metadata = metadata;
|
|
12102
12848
|
existing.clientLabel = clientLabel;
|
|
@@ -12145,10 +12891,10 @@ class ImageStorage {
|
|
|
12145
12891
|
return `${FILES_DIR}/${this.buildFilename(imageId, mime)}`;
|
|
12146
12892
|
}
|
|
12147
12893
|
getFilePath(imageId, mime) {
|
|
12148
|
-
return
|
|
12894
|
+
return import_node_path27.join(this.filesDir, this.buildFilename(imageId, mime));
|
|
12149
12895
|
}
|
|
12150
12896
|
resolveFile(relative) {
|
|
12151
|
-
return
|
|
12897
|
+
return import_node_path27.isAbsolute(relative) ? relative : import_node_path27.join(this.dir, relative);
|
|
12152
12898
|
}
|
|
12153
12899
|
findById(id) {
|
|
12154
12900
|
return this.index.images.find((e) => e.imageId === id);
|
|
@@ -12157,26 +12903,27 @@ class ImageStorage {
|
|
|
12157
12903
|
return [...this.index.images].sort((a, b) => b.metadata.created_at.localeCompare(a.metadata.created_at));
|
|
12158
12904
|
}
|
|
12159
12905
|
loadIndex() {
|
|
12160
|
-
if (!
|
|
12906
|
+
if (!import_node_fs31.existsSync(this.indexPath)) {
|
|
12161
12907
|
this.index = { images: [] };
|
|
12162
12908
|
return;
|
|
12163
12909
|
}
|
|
12164
12910
|
try {
|
|
12165
|
-
const raw = JSON.parse(
|
|
12911
|
+
const raw = JSON.parse(import_node_fs31.readFileSync(this.indexPath, "utf-8"));
|
|
12166
12912
|
this.index = raw && Array.isArray(raw.images) ? { images: raw.images } : { images: [] };
|
|
12167
12913
|
} catch {
|
|
12168
|
-
|
|
12914
|
+
const quarantinePath = quarantineCorruptFile(this.indexPath);
|
|
12915
|
+
this.logger.warn(quarantinePath ? `图片索引文件损坏,已隔离到 ${quarantinePath} 并重建空索引` : `图片索引文件损坏且隔离失败,已重建空索引: ${this.indexPath}`);
|
|
12169
12916
|
this.index = { images: [] };
|
|
12170
12917
|
}
|
|
12171
12918
|
}
|
|
12172
12919
|
saveIndex() {
|
|
12173
|
-
|
|
12920
|
+
writeFileAtomic(this.indexPath, JSON.stringify(this.index, null, 2));
|
|
12174
12921
|
}
|
|
12175
12922
|
async close() {}
|
|
12176
12923
|
}
|
|
12177
12924
|
// src/image/handler.ts
|
|
12178
|
-
var
|
|
12179
|
-
var
|
|
12925
|
+
var import_node_fs32 = require("node:fs");
|
|
12926
|
+
var import_node_path28 = require("node:path");
|
|
12180
12927
|
var DEFAULT_IMAGE_MAX_BYTES = 20 * 1024 * 1024;
|
|
12181
12928
|
var DOWNLOAD_TIMEOUT_MS = 5 * 60 * 1000;
|
|
12182
12929
|
function handleImageSync(imageId, metadata, storage2, maxBytes, logger) {
|
|
@@ -12209,13 +12956,13 @@ async function runImageDownloadInBackground(imageId, metadata, storage2, maxByte
|
|
|
12209
12956
|
logger.info(`[image-sync] 图片已同步: ${imageId} (${result.sizeBytes} bytes)`);
|
|
12210
12957
|
}
|
|
12211
12958
|
async function downloadImage(url, destPath, maxBytes, logger) {
|
|
12212
|
-
|
|
12959
|
+
import_node_fs32.mkdirSync(import_node_path28.dirname(destPath), { recursive: true });
|
|
12213
12960
|
const controller = new AbortController;
|
|
12214
12961
|
const timer = setTimeout(() => controller.abort(), DOWNLOAD_TIMEOUT_MS);
|
|
12215
12962
|
const cleanup = () => {
|
|
12216
12963
|
try {
|
|
12217
|
-
if (
|
|
12218
|
-
|
|
12964
|
+
if (import_node_fs32.existsSync(destPath))
|
|
12965
|
+
import_node_fs32.unlinkSync(destPath);
|
|
12219
12966
|
} catch {}
|
|
12220
12967
|
};
|
|
12221
12968
|
try {
|
|
@@ -12230,7 +12977,7 @@ async function downloadImage(url, destPath, maxBytes, logger) {
|
|
|
12230
12977
|
if (contentLength > 0 && contentLength > maxBytes) {
|
|
12231
12978
|
return { ok: false, error: `图片超过上限 ${maxBytes} 字节` };
|
|
12232
12979
|
}
|
|
12233
|
-
const writeStream =
|
|
12980
|
+
const writeStream = import_node_fs32.createWriteStream(destPath);
|
|
12234
12981
|
let total = 0;
|
|
12235
12982
|
try {
|
|
12236
12983
|
for await (const chunk of res.body) {
|
|
@@ -12264,19 +13011,19 @@ async function downloadImage(url, destPath, maxBytes, logger) {
|
|
|
12264
13011
|
}
|
|
12265
13012
|
}
|
|
12266
13013
|
// src/tunnel/service.ts
|
|
12267
|
-
var
|
|
12268
|
-
var
|
|
13014
|
+
var import_node_fs34 = require("node:fs");
|
|
13015
|
+
var import_node_path31 = require("node:path");
|
|
12269
13016
|
|
|
12270
13017
|
// src/tunnel/relay-client.ts
|
|
12271
|
-
var
|
|
12272
|
-
var
|
|
13018
|
+
var import_node_fs33 = require("node:fs");
|
|
13019
|
+
var import_node_path29 = require("node:path");
|
|
12273
13020
|
|
|
12274
13021
|
// node_modules/ws/wrapper.mjs
|
|
12275
13022
|
var import_stream = __toESM(require_stream(), 1);
|
|
12276
13023
|
var import_extension = __toESM(require_extension(), 1);
|
|
12277
13024
|
var import_permessage_deflate = __toESM(require_permessage_deflate(), 1);
|
|
12278
13025
|
var import_receiver = __toESM(require_receiver(), 1);
|
|
12279
|
-
var
|
|
13026
|
+
var import_sender3 = __toESM(require_sender(), 1);
|
|
12280
13027
|
var import_subprotocol = __toESM(require_subprotocol(), 1);
|
|
12281
13028
|
var import_websocket = __toESM(require_websocket(), 1);
|
|
12282
13029
|
var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
@@ -12386,8 +13133,8 @@ class RelayClient {
|
|
|
12386
13133
|
lastDisconnectReason
|
|
12387
13134
|
};
|
|
12388
13135
|
try {
|
|
12389
|
-
|
|
12390
|
-
|
|
13136
|
+
import_node_fs33.mkdirSync(import_node_path29.dirname(this.opts.statusFilePath), { recursive: true });
|
|
13137
|
+
import_node_fs33.writeFileSync(this.opts.statusFilePath, JSON.stringify(info, null, 2));
|
|
12391
13138
|
} catch {}
|
|
12392
13139
|
}
|
|
12393
13140
|
onInbound(handler) {
|
|
@@ -12789,13 +13536,13 @@ var import_node_crypto5 = require("node:crypto");
|
|
|
12789
13536
|
|
|
12790
13537
|
// src/host-sdk.ts
|
|
12791
13538
|
var import_node_module = require("node:module");
|
|
12792
|
-
var
|
|
13539
|
+
var import_node_path30 = require("node:path");
|
|
12793
13540
|
var import_node_url = require("node:url");
|
|
12794
13541
|
function resolveHostSdkModuleFromHostEntry(specifier, hostEntry = process.argv[1]) {
|
|
12795
13542
|
if (!hostEntry?.trim())
|
|
12796
13543
|
return "";
|
|
12797
13544
|
try {
|
|
12798
|
-
return import_node_module.createRequire(
|
|
13545
|
+
return import_node_module.createRequire(import_node_path30.resolve(hostEntry)).resolve(specifier);
|
|
12799
13546
|
} catch {
|
|
12800
13547
|
return "";
|
|
12801
13548
|
}
|
|
@@ -12988,6 +13735,33 @@ function loadOrCreateDeviceIdentity(stateDir) {
|
|
|
12988
13735
|
|
|
12989
13736
|
// src/tunnel/http-proxy.ts
|
|
12990
13737
|
var RELAY_INTERNAL_HTTP_HEADER = "x-openclaw-relay-internal";
|
|
13738
|
+
var GATEWAY_RESPONSE_HEADERS_TIMEOUT_MS = 30000;
|
|
13739
|
+
var LARGE_RESPONSE_BODY_WARN_CHARS = 512 * 1024;
|
|
13740
|
+
async function fetchWithHeadersTimeout(url, init, upstreamSignal, timeoutMs) {
|
|
13741
|
+
const controller = new AbortController;
|
|
13742
|
+
if (upstreamSignal) {
|
|
13743
|
+
if (upstreamSignal.aborted) {
|
|
13744
|
+
controller.abort(upstreamSignal.reason);
|
|
13745
|
+
} else {
|
|
13746
|
+
upstreamSignal.addEventListener("abort", () => controller.abort(upstreamSignal.reason), { once: true });
|
|
13747
|
+
}
|
|
13748
|
+
}
|
|
13749
|
+
let timedOut = false;
|
|
13750
|
+
const timer = setTimeout(() => {
|
|
13751
|
+
timedOut = true;
|
|
13752
|
+
controller.abort();
|
|
13753
|
+
}, timeoutMs);
|
|
13754
|
+
try {
|
|
13755
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
13756
|
+
} catch (err2) {
|
|
13757
|
+
if (timedOut) {
|
|
13758
|
+
throw new Error(`local gateway did not respond within ${timeoutMs}ms`);
|
|
13759
|
+
}
|
|
13760
|
+
throw err2;
|
|
13761
|
+
} finally {
|
|
13762
|
+
clearTimeout(timer);
|
|
13763
|
+
}
|
|
13764
|
+
}
|
|
12991
13765
|
var PATH_MAP = {
|
|
12992
13766
|
"/api/message/messageBridge/send": "/notifications"
|
|
12993
13767
|
};
|
|
@@ -13058,12 +13832,11 @@ async function handleHttpRequest(opts, frame) {
|
|
|
13058
13832
|
for (let attemptIndex = 0;attemptIndex < authAttempts.length; attemptIndex++) {
|
|
13059
13833
|
const attempt = authAttempts[attemptIndex];
|
|
13060
13834
|
opts.logger.info(`TunnelProxy: HTTP id=${frame.id} attempt ${attemptIndex + 1}/${authAttempts.length} auth=${attempt.label}`);
|
|
13061
|
-
const res = await
|
|
13835
|
+
const res = await fetchWithHeadersTimeout(url.toString(), {
|
|
13062
13836
|
method: frame.method,
|
|
13063
13837
|
headers: attempt.headers,
|
|
13064
|
-
signal: opts.abortSignal,
|
|
13065
13838
|
body: frame.method !== "GET" && frame.method !== "HEAD" ? frame.body : undefined
|
|
13066
|
-
});
|
|
13839
|
+
}, opts.abortSignal, GATEWAY_RESPONSE_HEADERS_TIMEOUT_MS);
|
|
13067
13840
|
const hasFallback = attemptIndex < authAttempts.length - 1;
|
|
13068
13841
|
if (res.status === 401 && hasFallback) {
|
|
13069
13842
|
const body = await res.text();
|
|
@@ -13103,6 +13876,9 @@ async function sendHttpResponse(opts, params) {
|
|
|
13103
13876
|
}
|
|
13104
13877
|
const body = await res.text();
|
|
13105
13878
|
opts.logger.info(`TunnelProxy: HTTP id=${frameId} response body=${previewText(body)}`);
|
|
13879
|
+
if (body.length > LARGE_RESPONSE_BODY_WARN_CHARS) {
|
|
13880
|
+
opts.logger.warn(`TunnelProxy: HTTP id=${frameId} ${method} ${path2} response body is ${body.length} chars; single proxy_response frame may exceed the app-side WebSocket message limit (~1MiB)`);
|
|
13881
|
+
}
|
|
13106
13882
|
const headers = {};
|
|
13107
13883
|
res.headers.forEach((value, key) => {
|
|
13108
13884
|
headers[key] = value;
|
|
@@ -13520,6 +14296,158 @@ function slimMessagesPayload(payload) {
|
|
|
13520
14296
|
}
|
|
13521
14297
|
return changed;
|
|
13522
14298
|
}
|
|
14299
|
+
var TOOL_MESSAGE_ROLES = new Set(["toolResult", "tool"]);
|
|
14300
|
+
var TOOL_CONTENT_TYPES = new Set([
|
|
14301
|
+
"toolResult",
|
|
14302
|
+
"tool_result",
|
|
14303
|
+
"toolUse",
|
|
14304
|
+
"tool_use",
|
|
14305
|
+
"toolCall",
|
|
14306
|
+
"tool_call",
|
|
14307
|
+
"thinking",
|
|
14308
|
+
"redacted_thinking",
|
|
14309
|
+
"reasoning"
|
|
14310
|
+
]);
|
|
14311
|
+
var MESSAGE_REASONING_FIELDS = [
|
|
14312
|
+
"thinking",
|
|
14313
|
+
"redactedThinking",
|
|
14314
|
+
"redacted_thinking",
|
|
14315
|
+
"reasoning",
|
|
14316
|
+
"reasoningContent",
|
|
14317
|
+
"reasoning_content",
|
|
14318
|
+
"thinkingSignature"
|
|
14319
|
+
];
|
|
14320
|
+
function stripReasoningFields(message) {
|
|
14321
|
+
if (!isRecord2(message))
|
|
14322
|
+
return { message, changed: false };
|
|
14323
|
+
if (!MESSAGE_REASONING_FIELDS.some((field) => (field in message))) {
|
|
14324
|
+
return { message, changed: false };
|
|
14325
|
+
}
|
|
14326
|
+
const next = { ...message };
|
|
14327
|
+
for (const field of MESSAGE_REASONING_FIELDS) {
|
|
14328
|
+
delete next[field];
|
|
14329
|
+
}
|
|
14330
|
+
return { message: next, changed: true };
|
|
14331
|
+
}
|
|
14332
|
+
function isToolMessage(message) {
|
|
14333
|
+
return isRecord2(message) && typeof message.role === "string" && TOOL_MESSAGE_ROLES.has(message.role);
|
|
14334
|
+
}
|
|
14335
|
+
function stripToolContentBlocks(content) {
|
|
14336
|
+
if (!Array.isArray(content))
|
|
14337
|
+
return { content, changed: false };
|
|
14338
|
+
const kept = content.filter((item) => !(isRecord2(item) && typeof item.type === "string" && TOOL_CONTENT_TYPES.has(item.type)));
|
|
14339
|
+
return { content: kept, changed: kept.length !== content.length };
|
|
14340
|
+
}
|
|
14341
|
+
function isEmptyMessageContent(content) {
|
|
14342
|
+
if (content == null)
|
|
14343
|
+
return true;
|
|
14344
|
+
if (typeof content === "string")
|
|
14345
|
+
return content.trim().length === 0;
|
|
14346
|
+
if (Array.isArray(content))
|
|
14347
|
+
return content.length === 0;
|
|
14348
|
+
return false;
|
|
14349
|
+
}
|
|
14350
|
+
function hasDisplayableMessageContent(message) {
|
|
14351
|
+
if (!isRecord2(message))
|
|
14352
|
+
return false;
|
|
14353
|
+
if (!isEmptyMessageContent(message.content))
|
|
14354
|
+
return true;
|
|
14355
|
+
return typeof message.text === "string" && message.text.trim().length > 0;
|
|
14356
|
+
}
|
|
14357
|
+
function messageText(message) {
|
|
14358
|
+
if (!isRecord2(message))
|
|
14359
|
+
return "";
|
|
14360
|
+
if (typeof message.content === "string")
|
|
14361
|
+
return message.content;
|
|
14362
|
+
if (!Array.isArray(message.content))
|
|
14363
|
+
return "";
|
|
14364
|
+
return message.content.filter((item) => isRecord2(item) && item.type === "text" && typeof item.text === "string").map((item) => item.text).join("");
|
|
14365
|
+
}
|
|
14366
|
+
function isInternalSubagentAnnounce(message) {
|
|
14367
|
+
if (!isRecord2(message))
|
|
14368
|
+
return false;
|
|
14369
|
+
const provenance = isRecord2(message.provenance) ? message.provenance : undefined;
|
|
14370
|
+
if (provenance?.kind === "inter_session" && provenance?.sourceTool === "subagent_announce") {
|
|
14371
|
+
return true;
|
|
14372
|
+
}
|
|
14373
|
+
const text = messageText(message);
|
|
14374
|
+
return message.role === "user" && text.includes("[Inter-session message]") && text.includes("sourceTool=subagent_announce") && text.includes("isUser=false");
|
|
14375
|
+
}
|
|
14376
|
+
function filterInternalMessages(messages) {
|
|
14377
|
+
const filtered = messages.filter((message) => !isInternalSubagentAnnounce(message));
|
|
14378
|
+
return {
|
|
14379
|
+
messages: filtered,
|
|
14380
|
+
removed: messages.length - filtered.length,
|
|
14381
|
+
changed: filtered.length !== messages.length
|
|
14382
|
+
};
|
|
14383
|
+
}
|
|
14384
|
+
function filterToolMessages(messages) {
|
|
14385
|
+
const result = [];
|
|
14386
|
+
let changed = false;
|
|
14387
|
+
for (const message of messages) {
|
|
14388
|
+
if (isToolMessage(message)) {
|
|
14389
|
+
changed = true;
|
|
14390
|
+
continue;
|
|
14391
|
+
}
|
|
14392
|
+
const stripped = stripToolContentFromMessage(message);
|
|
14393
|
+
if (stripped.changed) {
|
|
14394
|
+
changed = true;
|
|
14395
|
+
if (!hasDisplayableMessageContent(stripped.message))
|
|
14396
|
+
continue;
|
|
14397
|
+
result.push(stripped.message);
|
|
14398
|
+
continue;
|
|
14399
|
+
}
|
|
14400
|
+
result.push(message);
|
|
14401
|
+
}
|
|
14402
|
+
return { messages: result, removed: messages.length - result.length, changed };
|
|
14403
|
+
}
|
|
14404
|
+
function stripToolContentFromMessage(message) {
|
|
14405
|
+
const reasoningStripped = stripReasoningFields(message);
|
|
14406
|
+
let next = reasoningStripped.message;
|
|
14407
|
+
let changed = reasoningStripped.changed;
|
|
14408
|
+
if (isRecord2(next) && Array.isArray(next.content)) {
|
|
14409
|
+
const stripped = stripToolContentBlocks(next.content);
|
|
14410
|
+
if (stripped.changed) {
|
|
14411
|
+
next = { ...next, content: stripped.content };
|
|
14412
|
+
changed = true;
|
|
14413
|
+
}
|
|
14414
|
+
}
|
|
14415
|
+
return changed ? { message: next, changed } : { message, changed: false };
|
|
14416
|
+
}
|
|
14417
|
+
function rewriteSessionEventFrame(frame) {
|
|
14418
|
+
if (frame?.type !== "event")
|
|
14419
|
+
return { action: "keep" };
|
|
14420
|
+
if (frame.event === "session.tool") {
|
|
14421
|
+
return { action: "drop", reason: "tool event mirror" };
|
|
14422
|
+
}
|
|
14423
|
+
if (frame.event !== "session.message")
|
|
14424
|
+
return { action: "keep" };
|
|
14425
|
+
const message = frame?.payload?.message;
|
|
14426
|
+
if (!isRecord2(message))
|
|
14427
|
+
return { action: "keep" };
|
|
14428
|
+
if (isToolMessage(message)) {
|
|
14429
|
+
return { action: "drop", reason: "tool message" };
|
|
14430
|
+
}
|
|
14431
|
+
if (isInternalSubagentAnnounce(message)) {
|
|
14432
|
+
return { action: "drop", reason: "internal subagent announce" };
|
|
14433
|
+
}
|
|
14434
|
+
const stripped = stripToolContentFromMessage(message);
|
|
14435
|
+
if (!stripped.changed)
|
|
14436
|
+
return { action: "keep" };
|
|
14437
|
+
if (!hasDisplayableMessageContent(stripped.message)) {
|
|
14438
|
+
return { action: "drop", reason: "no displayable content after strip" };
|
|
14439
|
+
}
|
|
14440
|
+
return {
|
|
14441
|
+
action: "rewrite",
|
|
14442
|
+
frame: {
|
|
14443
|
+
...frame,
|
|
14444
|
+
payload: {
|
|
14445
|
+
...frame.payload,
|
|
14446
|
+
message: stripped.message
|
|
14447
|
+
}
|
|
14448
|
+
}
|
|
14449
|
+
};
|
|
14450
|
+
}
|
|
13523
14451
|
function slimSessionsListPayload(payload) {
|
|
13524
14452
|
if (!Array.isArray(payload?.sessions))
|
|
13525
14453
|
return false;
|
|
@@ -13611,6 +14539,27 @@ function slimConfigGetPayload(payload) {
|
|
|
13611
14539
|
rawPlaceholder: CONFIG_RAW_PLACEHOLDER
|
|
13612
14540
|
});
|
|
13613
14541
|
}
|
|
14542
|
+
var SUPPRESSED_AGENT_STREAMS = new Set([
|
|
14543
|
+
"command_output",
|
|
14544
|
+
"tool",
|
|
14545
|
+
"thinking",
|
|
14546
|
+
"thought"
|
|
14547
|
+
]);
|
|
14548
|
+
var TOOL_ITEM_KINDS = new Set(["command", "tool"]);
|
|
14549
|
+
function isSuppressedAgentEventFrame(frame) {
|
|
14550
|
+
if (frame?.type !== "event" || frame?.event !== "agent")
|
|
14551
|
+
return false;
|
|
14552
|
+
const payload = frame.payload;
|
|
14553
|
+
if (!isRecord2(payload) || typeof payload.stream !== "string")
|
|
14554
|
+
return false;
|
|
14555
|
+
if (SUPPRESSED_AGENT_STREAMS.has(payload.stream))
|
|
14556
|
+
return true;
|
|
14557
|
+
if (payload.stream === "item") {
|
|
14558
|
+
const kind = isRecord2(payload.data) ? payload.data.kind : undefined;
|
|
14559
|
+
return typeof kind === "string" && TOOL_ITEM_KINDS.has(kind);
|
|
14560
|
+
}
|
|
14561
|
+
return false;
|
|
14562
|
+
}
|
|
13614
14563
|
function slimRelayFrame(frame) {
|
|
13615
14564
|
if (frame?.type === "event" && frame.event === "agent") {
|
|
13616
14565
|
return slimAgentEventPayload(frame.payload);
|
|
@@ -13624,6 +14573,15 @@ function slimRelayFrame(frame) {
|
|
|
13624
14573
|
}
|
|
13625
14574
|
return false;
|
|
13626
14575
|
}
|
|
14576
|
+
function slimParsedRelayFrame(text, frame, logger) {
|
|
14577
|
+
if (text.length <= SLIM_TRIGGER_CHARS)
|
|
14578
|
+
return text;
|
|
14579
|
+
if (!slimRelayFrame(frame))
|
|
14580
|
+
return text;
|
|
14581
|
+
const slimmed = JSON.stringify(frame);
|
|
14582
|
+
logger?.info(`TunnelProxy: slimmed relay-bound frame ${text.length} → ${slimmed.length} chars`);
|
|
14583
|
+
return slimmed;
|
|
14584
|
+
}
|
|
13627
14585
|
function slimRelayFrameText(text, logger) {
|
|
13628
14586
|
if (text.length <= SLIM_TRIGGER_CHARS)
|
|
13629
14587
|
return text;
|
|
@@ -13633,11 +14591,7 @@ function slimRelayFrameText(text, logger) {
|
|
|
13633
14591
|
} catch {
|
|
13634
14592
|
return text;
|
|
13635
14593
|
}
|
|
13636
|
-
|
|
13637
|
-
return text;
|
|
13638
|
-
const slimmed = JSON.stringify(frame);
|
|
13639
|
-
logger?.info(`TunnelProxy: slimmed relay-bound frame ${text.length} → ${slimmed.length} chars`);
|
|
13640
|
-
return slimmed;
|
|
14594
|
+
return slimParsedRelayFrame(text, frame, logger);
|
|
13641
14595
|
}
|
|
13642
14596
|
|
|
13643
14597
|
// src/tunnel/proxy.ts
|
|
@@ -13655,6 +14609,9 @@ var MAX_AUTO_PAIRING_APPROVALS = 3;
|
|
|
13655
14609
|
var RECENT_ABORTED_CHAT_RUN_TTL_MS = 60000;
|
|
13656
14610
|
var GATEWAY_RPC_HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
13657
14611
|
var MAX_GATEWAY_WS_PENDING = 200;
|
|
14612
|
+
var GATEWAY_RPC_CONNECT_LOG_INTERVAL_MS = 1e4;
|
|
14613
|
+
var GATEWAY_RPC_RECONNECT_BASE_MS = 1000;
|
|
14614
|
+
var GATEWAY_RPC_RECONNECT_MAX_MS = 30000;
|
|
13658
14615
|
var approveDevicePairingPromise = null;
|
|
13659
14616
|
var approveDevicePairingWarned = false;
|
|
13660
14617
|
function formatErrorMessage(err2) {
|
|
@@ -13692,6 +14649,12 @@ class TunnelProxy {
|
|
|
13692
14649
|
gatewayReqMetaById = new Map;
|
|
13693
14650
|
recentAbortedChatRuns = new Map;
|
|
13694
14651
|
httpAbortControllers = new Map;
|
|
14652
|
+
gatewayWsConnectLogLastAt = 0;
|
|
14653
|
+
gatewayWsConnectLogSuppressed = 0;
|
|
14654
|
+
maintainGatewayWs = false;
|
|
14655
|
+
gatewayWsReconnectTimer = null;
|
|
14656
|
+
gatewayWsReconnectAttempt = 0;
|
|
14657
|
+
gatewayConnectionState = null;
|
|
13695
14658
|
deviceIdentity;
|
|
13696
14659
|
stateDir;
|
|
13697
14660
|
hostStateDir;
|
|
@@ -13732,14 +14695,22 @@ class TunnelProxy {
|
|
|
13732
14695
|
break;
|
|
13733
14696
|
}
|
|
13734
14697
|
}
|
|
13735
|
-
|
|
13736
|
-
this.opts.logger.info(`TunnelProxy: cleanup, closing ${this.wsProxy.activeCount} active WS connections, gatewayWs=${!!this.gatewayWs}`);
|
|
14698
|
+
cleanupRelayState() {
|
|
14699
|
+
this.opts.logger.info(`TunnelProxy: relay cleanup, closing ${this.wsProxy.activeCount} active WS connections, gatewayWs=${!!this.gatewayWs}`);
|
|
13737
14700
|
this.wsProxy.cleanup();
|
|
13738
14701
|
for (const [id, controller] of this.httpAbortControllers) {
|
|
13739
14702
|
this.opts.logger.info(`TunnelProxy: aborting HTTP proxy id=${id}`);
|
|
13740
14703
|
controller.abort();
|
|
13741
14704
|
}
|
|
13742
14705
|
this.httpAbortControllers.clear();
|
|
14706
|
+
this.gatewayWsPending = [];
|
|
14707
|
+
this.gatewayReqMetaById.clear();
|
|
14708
|
+
this.recentAbortedChatRuns.clear();
|
|
14709
|
+
}
|
|
14710
|
+
cleanup() {
|
|
14711
|
+
this.cleanupRelayState();
|
|
14712
|
+
this.maintainGatewayWs = false;
|
|
14713
|
+
this.clearGatewayReconnectTimer();
|
|
13743
14714
|
const gatewayWs = this.gatewayWs;
|
|
13744
14715
|
this.gatewayWs = null;
|
|
13745
14716
|
this.gatewayWsReady = false;
|
|
@@ -13747,14 +14718,19 @@ class TunnelProxy {
|
|
|
13747
14718
|
this.gatewayWsReconnectRequested = false;
|
|
13748
14719
|
this.gatewayWsPairingApprovalPending = false;
|
|
13749
14720
|
this.gatewayWsAutoPairingApprovals = 0;
|
|
13750
|
-
this.
|
|
13751
|
-
this.gatewayReqMetaById.clear();
|
|
13752
|
-
this.recentAbortedChatRuns.clear();
|
|
14721
|
+
const wasConnected = this.gatewayConnectionState === "connected";
|
|
13753
14722
|
if (gatewayWs) {
|
|
13754
14723
|
try {
|
|
13755
14724
|
gatewayWs.close();
|
|
13756
14725
|
} catch {}
|
|
13757
14726
|
}
|
|
14727
|
+
if (wasConnected) {
|
|
14728
|
+
this.setGatewayConnectionState("disconnected", "plugin stopping");
|
|
14729
|
+
}
|
|
14730
|
+
}
|
|
14731
|
+
startGatewayConnection() {
|
|
14732
|
+
this.maintainGatewayWs = true;
|
|
14733
|
+
this.ensureGatewayWs();
|
|
13758
14734
|
}
|
|
13759
14735
|
async handleRequestFrame(frame) {
|
|
13760
14736
|
const controller = new AbortController;
|
|
@@ -14020,6 +14996,21 @@ class TunnelProxy {
|
|
|
14020
14996
|
}
|
|
14021
14997
|
maybeRewriteGatewayFrame(text, frame) {
|
|
14022
14998
|
this.pruneRecentAbortedChatRuns();
|
|
14999
|
+
if (isSuppressedAgentEventFrame(frame)) {
|
|
15000
|
+
const stream = frame?.payload?.stream;
|
|
15001
|
+
const kind = frame?.payload?.data?.kind;
|
|
15002
|
+
this.opts.logger.info(`TunnelProxy: dropping agent event stream=${stream}${kind ? ` kind=${kind}` : ""} for session=${frame?.payload?.sessionKey ?? "unknown"}`);
|
|
15003
|
+
return null;
|
|
15004
|
+
}
|
|
15005
|
+
const sessionEvent = rewriteSessionEventFrame(frame);
|
|
15006
|
+
if (sessionEvent.action === "drop") {
|
|
15007
|
+
this.opts.logger.info(`TunnelProxy: dropping ${frame?.event} event (${sessionEvent.reason}) for session=${frame?.payload?.sessionKey ?? "unknown"}`);
|
|
15008
|
+
return null;
|
|
15009
|
+
}
|
|
15010
|
+
if (sessionEvent.action === "rewrite") {
|
|
15011
|
+
this.opts.logger.info(`TunnelProxy: stripped thinking/tool content blocks from session.message for session=${frame?.payload?.sessionKey ?? "unknown"}`);
|
|
15012
|
+
return JSON.stringify(sessionEvent.frame);
|
|
15013
|
+
}
|
|
14023
15014
|
if (frame?.type === "event" && frame?.event === "chat" && frame?.payload?.state === "aborted") {
|
|
14024
15015
|
this.rememberRecentAbortedChatRun(frame.payload?.runId, frame.payload?.sessionKey);
|
|
14025
15016
|
return text;
|
|
@@ -14048,22 +15039,36 @@ class TunnelProxy {
|
|
|
14048
15039
|
this.opts.logger.info(`TunnelProxy: suppressing synthetic abort failure final for runId=${runId}`);
|
|
14049
15040
|
return null;
|
|
14050
15041
|
}
|
|
15042
|
+
const stripped = stripToolContentFromMessage(frame?.payload?.message);
|
|
15043
|
+
if (stripped.changed) {
|
|
15044
|
+
this.opts.logger.info(`TunnelProxy: stripped thinking/tool content blocks from chat final for runId=${runId ?? "unknown"}`);
|
|
15045
|
+
return JSON.stringify({
|
|
15046
|
+
...frame,
|
|
15047
|
+
payload: { ...frame.payload, message: stripped.message }
|
|
15048
|
+
});
|
|
15049
|
+
}
|
|
14051
15050
|
return text;
|
|
14052
15051
|
}
|
|
14053
15052
|
if (frame?.type === "res" && frame?.ok === true && reqMeta?.method === "chat.history" && Array.isArray(frame?.payload?.messages)) {
|
|
14054
15053
|
const sessionKey = this.normalizeSessionKey(frame?.payload?.sessionKey) ?? reqMeta.sessionKey;
|
|
14055
|
-
|
|
14056
|
-
|
|
14057
|
-
|
|
14058
|
-
|
|
15054
|
+
const originalCount = frame.payload.messages.length;
|
|
15055
|
+
let messages = frame.payload.messages;
|
|
15056
|
+
if (sessionKey) {
|
|
15057
|
+
messages = messages.filter((message) => !this.isSyntheticAbortHistoryMessage(message, sessionKey));
|
|
15058
|
+
}
|
|
15059
|
+
const internalFiltered = filterInternalMessages(messages);
|
|
15060
|
+
messages = internalFiltered.messages;
|
|
15061
|
+
const toolFiltered = filterToolMessages(messages);
|
|
15062
|
+
messages = toolFiltered.messages;
|
|
15063
|
+
if (messages.length === originalCount && !internalFiltered.changed && !toolFiltered.changed) {
|
|
14059
15064
|
return text;
|
|
14060
15065
|
}
|
|
14061
|
-
this.opts.logger.info(`TunnelProxy:
|
|
15066
|
+
this.opts.logger.info(`TunnelProxy: chat.history filtered ${originalCount - messages.length} message(s) (tool+internal+synthetic) for session=${sessionKey ?? "unknown"}`);
|
|
14062
15067
|
return JSON.stringify({
|
|
14063
15068
|
...frame,
|
|
14064
15069
|
payload: {
|
|
14065
15070
|
...frame.payload,
|
|
14066
|
-
messages
|
|
15071
|
+
messages
|
|
14067
15072
|
}
|
|
14068
15073
|
});
|
|
14069
15074
|
}
|
|
@@ -14074,10 +15079,11 @@ class TunnelProxy {
|
|
|
14074
15079
|
return;
|
|
14075
15080
|
if (this.gatewayWsConnecting)
|
|
14076
15081
|
return;
|
|
15082
|
+
this.clearGatewayReconnectTimer();
|
|
14077
15083
|
this.gatewayWsConnecting = true;
|
|
14078
15084
|
this.gatewayWsReady = false;
|
|
14079
15085
|
const wsUrl = this.opts.gatewayBaseUrl.replace(/^http/, "ws");
|
|
14080
|
-
this.
|
|
15086
|
+
this.logGatewayWsConnecting(wsUrl);
|
|
14081
15087
|
let ws;
|
|
14082
15088
|
try {
|
|
14083
15089
|
ws = new wrapper_default(wsUrl, {
|
|
@@ -14087,7 +15093,9 @@ class TunnelProxy {
|
|
|
14087
15093
|
const message = formatErrorMessage(err2);
|
|
14088
15094
|
this.gatewayWsConnecting = false;
|
|
14089
15095
|
this.opts.logger.error(`TunnelProxy: RPC WS failed to create gateway connection: ${message}`);
|
|
15096
|
+
this.setGatewayConnectionState("disconnected", message);
|
|
14090
15097
|
this.failAllGatewayRequests("GATEWAY_RPC_CONNECT_FAILED", `local gateway RPC connect failed: ${message}`);
|
|
15098
|
+
this.scheduleGatewayReconnect(message);
|
|
14091
15099
|
return;
|
|
14092
15100
|
}
|
|
14093
15101
|
this.gatewayWs = ws;
|
|
@@ -14095,10 +15103,11 @@ class TunnelProxy {
|
|
|
14095
15103
|
handshakeTimer = null;
|
|
14096
15104
|
if (this.gatewayWs !== ws || this.gatewayWsReady)
|
|
14097
15105
|
return;
|
|
14098
|
-
this.opts.logger.warn(`TunnelProxy: RPC WS handshake timed out after ${GATEWAY_RPC_HANDSHAKE_TIMEOUT_MS}ms (pending=${this.gatewayWsPending.length})`);
|
|
15106
|
+
this.opts.logger.warn(`TunnelProxy: RPC WS handshake timed out after ${GATEWAY_RPC_HANDSHAKE_TIMEOUT_MS}ms (pending=${this.gatewayWsPending.length}); ${this.gatewayTroubleshootingHint()}`);
|
|
14099
15107
|
this.gatewayWs = null;
|
|
14100
15108
|
this.gatewayWsReady = false;
|
|
14101
15109
|
this.gatewayWsConnecting = false;
|
|
15110
|
+
this.setGatewayConnectionState("disconnected", "gateway RPC handshake timed out");
|
|
14102
15111
|
this.failAllGatewayRequests("GATEWAY_RPC_HANDSHAKE_TIMEOUT", "local gateway RPC handshake timed out");
|
|
14103
15112
|
try {
|
|
14104
15113
|
if (typeof ws.terminate === "function") {
|
|
@@ -14107,6 +15116,7 @@ class TunnelProxy {
|
|
|
14107
15116
|
ws.close();
|
|
14108
15117
|
}
|
|
14109
15118
|
} catch {}
|
|
15119
|
+
this.scheduleGatewayReconnect("gateway RPC handshake timed out");
|
|
14110
15120
|
}, GATEWAY_RPC_HANDSHAKE_TIMEOUT_MS);
|
|
14111
15121
|
const clearHandshakeTimer = () => {
|
|
14112
15122
|
if (handshakeTimer) {
|
|
@@ -14150,6 +15160,9 @@ class TunnelProxy {
|
|
|
14150
15160
|
this.gatewayWsReconnectRequested = false;
|
|
14151
15161
|
this.gatewayWsReady = true;
|
|
14152
15162
|
this.gatewayWsConnecting = false;
|
|
15163
|
+
this.gatewayWsReconnectAttempt = 0;
|
|
15164
|
+
this.clearGatewayReconnectTimer();
|
|
15165
|
+
this.setGatewayConnectionState("connected", "hello-ok");
|
|
14153
15166
|
this.opts.logger.info(`TunnelProxy: RPC WS handshake done (hello-ok), flushing ${this.gatewayWsPending.length} pending frames`);
|
|
14154
15167
|
const pending = this.gatewayWsPending;
|
|
14155
15168
|
this.gatewayWsPending = [];
|
|
@@ -14175,13 +15188,19 @@ class TunnelProxy {
|
|
|
14175
15188
|
return;
|
|
14176
15189
|
}
|
|
14177
15190
|
this.opts.logger.error(`TunnelProxy: RPC WS handshake failed (pending=${this.gatewayWsPending.length}): ${previewText(JSON.stringify(frame.error), 500)}`);
|
|
15191
|
+
this.setGatewayConnectionState("disconnected", "gateway RPC handshake failed");
|
|
14178
15192
|
clearHandshakeTimer();
|
|
14179
15193
|
this.failAllGatewayRequests("GATEWAY_RPC_HANDSHAKE_FAILED", `local gateway RPC handshake failed: ${previewText(JSON.stringify(frame.error), 500)}`);
|
|
14180
15194
|
ws.close();
|
|
14181
15195
|
return;
|
|
14182
15196
|
}
|
|
14183
15197
|
const rewritten = this.maybeRewriteGatewayFrame(text, frame);
|
|
14184
|
-
if (rewritten
|
|
15198
|
+
if (rewritten === null) {
|
|
15199
|
+
return;
|
|
15200
|
+
}
|
|
15201
|
+
if (rewritten === text) {
|
|
15202
|
+
this.opts.client.sendRaw(slimParsedRelayFrame(text, frame, this.opts.logger));
|
|
15203
|
+
} else {
|
|
14185
15204
|
this.opts.client.sendRaw(slimRelayFrameText(rewritten, this.opts.logger));
|
|
14186
15205
|
}
|
|
14187
15206
|
});
|
|
@@ -14198,6 +15217,9 @@ class TunnelProxy {
|
|
|
14198
15217
|
this.gatewayWsReady = false;
|
|
14199
15218
|
}
|
|
14200
15219
|
this.gatewayWsConnecting = false;
|
|
15220
|
+
if (wasReady || this.maintainGatewayWs && !shouldReconnect && !shouldHoldForPairing) {
|
|
15221
|
+
this.setGatewayConnectionState("disconnected", `gateway closed: code=${code}, reason=${reasonText}`);
|
|
15222
|
+
}
|
|
14201
15223
|
this.maybeClearStoredDeviceTokenOnMismatch(code, reasonText);
|
|
14202
15224
|
if (shouldReconnect) {
|
|
14203
15225
|
this.gatewayWsReconnectRequested = false;
|
|
@@ -14206,16 +15228,20 @@ class TunnelProxy {
|
|
|
14206
15228
|
} else if (!shouldHoldForPairing && (pendingCount > 0 || this.gatewayReqMetaById.size > 0)) {
|
|
14207
15229
|
this.failAllGatewayRequests("GATEWAY_RPC_DISCONNECTED", `local gateway RPC disconnected: code=${code}, reason=${reasonText}`);
|
|
14208
15230
|
}
|
|
15231
|
+
if (!shouldReconnect && !shouldHoldForPairing) {
|
|
15232
|
+
this.scheduleGatewayReconnect(`gateway closed: code=${code}, reason=${reasonText}`);
|
|
15233
|
+
}
|
|
14209
15234
|
});
|
|
14210
15235
|
ws.on("error", (err2) => {
|
|
14211
15236
|
clearHandshakeTimer();
|
|
14212
|
-
this.opts.logger.warn(`TunnelProxy: RPC WS error: ${err2.message} (ready=${this.gatewayWsReady}, pending=${this.gatewayWsPending.length}, activeWs=${this.wsProxy.activeCount})`);
|
|
15237
|
+
this.opts.logger.warn(`TunnelProxy: RPC WS error: ${err2.message} (ready=${this.gatewayWsReady}, pending=${this.gatewayWsPending.length}, activeWs=${this.wsProxy.activeCount}); ${this.gatewayTroubleshootingHint()}`);
|
|
14213
15238
|
const shouldFailRequests = this.gatewayWs === ws && (this.gatewayWsPending.length > 0 || this.gatewayReqMetaById.size > 0);
|
|
14214
15239
|
this.gatewayWsConnecting = false;
|
|
14215
15240
|
if (this.gatewayWs === ws) {
|
|
14216
15241
|
this.gatewayWs = null;
|
|
14217
15242
|
this.gatewayWsReady = false;
|
|
14218
15243
|
}
|
|
15244
|
+
this.setGatewayConnectionState("disconnected", `gateway RPC error: ${err2.message}`);
|
|
14219
15245
|
if (shouldFailRequests) {
|
|
14220
15246
|
this.failAllGatewayRequests("GATEWAY_RPC_ERROR", `local gateway RPC error: ${err2.message}`);
|
|
14221
15247
|
}
|
|
@@ -14224,8 +15250,58 @@ class TunnelProxy {
|
|
|
14224
15250
|
ws.terminate();
|
|
14225
15251
|
}
|
|
14226
15252
|
} catch {}
|
|
15253
|
+
this.scheduleGatewayReconnect(`gateway RPC error: ${err2.message}`);
|
|
14227
15254
|
});
|
|
14228
15255
|
}
|
|
15256
|
+
setGatewayConnectionState(state, reason) {
|
|
15257
|
+
if (this.gatewayConnectionState === state)
|
|
15258
|
+
return;
|
|
15259
|
+
this.gatewayConnectionState = state;
|
|
15260
|
+
if (state === "connected") {
|
|
15261
|
+
this.opts.logger.info(`TunnelProxy: local gateway connection state connected (${reason})`);
|
|
15262
|
+
this.opts.onGatewayConnected?.();
|
|
15263
|
+
return;
|
|
15264
|
+
}
|
|
15265
|
+
this.opts.logger.warn(`TunnelProxy: local gateway connection state disconnected (${reason})`);
|
|
15266
|
+
this.opts.onGatewayDisconnected?.(reason);
|
|
15267
|
+
}
|
|
15268
|
+
clearGatewayReconnectTimer() {
|
|
15269
|
+
if (!this.gatewayWsReconnectTimer)
|
|
15270
|
+
return;
|
|
15271
|
+
clearTimeout(this.gatewayWsReconnectTimer);
|
|
15272
|
+
this.gatewayWsReconnectTimer = null;
|
|
15273
|
+
}
|
|
15274
|
+
scheduleGatewayReconnect(reason) {
|
|
15275
|
+
if (!this.maintainGatewayWs || this.gatewayWsReconnectTimer)
|
|
15276
|
+
return;
|
|
15277
|
+
if (this.gatewayWsReady || this.gatewayWsConnecting)
|
|
15278
|
+
return;
|
|
15279
|
+
const baseDelayMs = Math.min(GATEWAY_RPC_RECONNECT_BASE_MS * Math.pow(2, this.gatewayWsReconnectAttempt), GATEWAY_RPC_RECONNECT_MAX_MS);
|
|
15280
|
+
const jitterMs = Math.floor(Math.random() * Math.max(1, baseDelayMs / 4));
|
|
15281
|
+
const delayMs = baseDelayMs + jitterMs;
|
|
15282
|
+
this.gatewayWsReconnectAttempt++;
|
|
15283
|
+
this.opts.logger.info(`TunnelProxy: reconnecting local gateway RPC in ${delayMs}ms (attempt ${this.gatewayWsReconnectAttempt}, reason=${reason})`);
|
|
15284
|
+
this.gatewayWsReconnectTimer = setTimeout(() => {
|
|
15285
|
+
this.gatewayWsReconnectTimer = null;
|
|
15286
|
+
this.ensureGatewayWs();
|
|
15287
|
+
}, delayMs);
|
|
15288
|
+
this.gatewayWsReconnectTimer.unref?.();
|
|
15289
|
+
}
|
|
15290
|
+
logGatewayWsConnecting(wsUrl) {
|
|
15291
|
+
const now = Date.now();
|
|
15292
|
+
const shouldLog = this.gatewayWsConnectLogLastAt === 0 || now - this.gatewayWsConnectLogLastAt >= GATEWAY_RPC_CONNECT_LOG_INTERVAL_MS;
|
|
15293
|
+
if (!shouldLog) {
|
|
15294
|
+
this.gatewayWsConnectLogSuppressed++;
|
|
15295
|
+
return;
|
|
15296
|
+
}
|
|
15297
|
+
const suppressed = this.gatewayWsConnectLogSuppressed > 0 ? `, suppressed=${this.gatewayWsConnectLogSuppressed}` : "";
|
|
15298
|
+
this.opts.logger.info(`TunnelProxy: RPC WS connecting to gateway ${wsUrl} (pending=${this.gatewayWsPending.length}${suppressed})`);
|
|
15299
|
+
this.gatewayWsConnectLogLastAt = now;
|
|
15300
|
+
this.gatewayWsConnectLogSuppressed = 0;
|
|
15301
|
+
}
|
|
15302
|
+
gatewayTroubleshootingHint() {
|
|
15303
|
+
return `check local gateway (${this.opts.gatewayBaseUrl}), openclaw gateway status, gateway.port/gateway.tls.enabled, or PHONE_NOTIFICATIONS_GATEWAY_BASE_URL`;
|
|
15304
|
+
}
|
|
14229
15305
|
handleConnectChallenge(ws, frame) {
|
|
14230
15306
|
const challengeNonce = frame.payload?.nonce ?? "";
|
|
14231
15307
|
const connectRequestId = `tunnel-connect-${import_node_crypto5.randomUUID()}`;
|
|
@@ -14278,57 +15354,105 @@ class TunnelProxy {
|
|
|
14278
15354
|
}
|
|
14279
15355
|
|
|
14280
15356
|
// src/tunnel/connect-status-reporter.ts
|
|
15357
|
+
var REPORT_TIMEOUT_MS = 1e4;
|
|
15358
|
+
var RETRY_BASE_MS = 1000;
|
|
15359
|
+
var RETRY_MAX_MS = 60000;
|
|
14281
15360
|
function createConnectStatusReporter(opts) {
|
|
14282
15361
|
const { logger } = opts;
|
|
14283
15362
|
let lastReported = null;
|
|
15363
|
+
let desiredStatus = null;
|
|
14284
15364
|
let inFlight = null;
|
|
15365
|
+
let retryTimer = null;
|
|
15366
|
+
let retryAttempt = 0;
|
|
14285
15367
|
async function post(status) {
|
|
14286
15368
|
const url = getEnvUrls().gatewayConnectStatusUrl;
|
|
14287
15369
|
if (!url) {
|
|
14288
15370
|
logger.warn("[gateway-connect-status] url is empty for current env, skip report");
|
|
14289
|
-
return;
|
|
15371
|
+
return "skip";
|
|
14290
15372
|
}
|
|
14291
15373
|
const apiKey = loadApiKey();
|
|
14292
15374
|
if (!apiKey) {
|
|
14293
15375
|
logger.info("[gateway-connect-status] api key missing, skip report");
|
|
14294
|
-
return;
|
|
15376
|
+
return "skip";
|
|
14295
15377
|
}
|
|
14296
15378
|
const rawApiKey = apiKey.startsWith("Bearer ") ? apiKey.slice("Bearer ".length) : apiKey;
|
|
14297
15379
|
try {
|
|
14298
15380
|
const res = await fetch(url, {
|
|
14299
15381
|
method: "POST",
|
|
14300
15382
|
headers: { "Content-Type": "application/json" },
|
|
14301
|
-
body: JSON.stringify({ apiKey: rawApiKey, connectStatus: status })
|
|
15383
|
+
body: JSON.stringify({ apiKey: rawApiKey, connectStatus: status }),
|
|
15384
|
+
signal: AbortSignal.timeout(REPORT_TIMEOUT_MS)
|
|
14302
15385
|
});
|
|
14303
15386
|
if (!res.ok) {
|
|
14304
15387
|
logger.warn(`[gateway-connect-status] report ${status} failed: HTTP ${res.status} ${res.statusText}`);
|
|
14305
|
-
return;
|
|
15388
|
+
return res.status >= 500 || res.status === 429 ? "retry" : "skip";
|
|
14306
15389
|
}
|
|
14307
15390
|
logger.info(`[gateway-connect-status] reported ${status}`);
|
|
15391
|
+
return "success";
|
|
14308
15392
|
} catch (err2) {
|
|
14309
15393
|
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
14310
15394
|
logger.warn(`[gateway-connect-status] report ${status} error: ${message}`);
|
|
15395
|
+
return "retry";
|
|
15396
|
+
}
|
|
15397
|
+
}
|
|
15398
|
+
function clearRetryTimer() {
|
|
15399
|
+
if (!retryTimer)
|
|
15400
|
+
return;
|
|
15401
|
+
clearTimeout(retryTimer);
|
|
15402
|
+
retryTimer = null;
|
|
15403
|
+
}
|
|
15404
|
+
function scheduleRetry() {
|
|
15405
|
+
if (retryTimer || desiredStatus === null || desiredStatus === lastReported) {
|
|
15406
|
+
return;
|
|
14311
15407
|
}
|
|
15408
|
+
const delayMs = Math.min(RETRY_BASE_MS * Math.pow(2, retryAttempt), RETRY_MAX_MS);
|
|
15409
|
+
retryAttempt++;
|
|
15410
|
+
logger.info(`[gateway-connect-status] retrying ${desiredStatus} in ${delayMs}ms (attempt ${retryAttempt})`);
|
|
15411
|
+
retryTimer = setTimeout(() => {
|
|
15412
|
+
retryTimer = null;
|
|
15413
|
+
drain();
|
|
15414
|
+
}, delayMs);
|
|
15415
|
+
retryTimer.unref?.();
|
|
15416
|
+
}
|
|
15417
|
+
function drain() {
|
|
15418
|
+
if (inFlight || desiredStatus === null || desiredStatus === lastReported) {
|
|
15419
|
+
return;
|
|
15420
|
+
}
|
|
15421
|
+
clearRetryTimer();
|
|
15422
|
+
const status = desiredStatus;
|
|
15423
|
+
inFlight = post(status).then((result) => {
|
|
15424
|
+
if (result === "success") {
|
|
15425
|
+
lastReported = status;
|
|
15426
|
+
retryAttempt = 0;
|
|
15427
|
+
} else if (result === "skip" && desiredStatus === status) {
|
|
15428
|
+
desiredStatus = null;
|
|
15429
|
+
retryAttempt = 0;
|
|
15430
|
+
}
|
|
15431
|
+
}).finally(() => {
|
|
15432
|
+
inFlight = null;
|
|
15433
|
+
if (desiredStatus === lastReported)
|
|
15434
|
+
return;
|
|
15435
|
+
if (desiredStatus !== status) {
|
|
15436
|
+
retryAttempt = 0;
|
|
15437
|
+
drain();
|
|
15438
|
+
return;
|
|
15439
|
+
}
|
|
15440
|
+
scheduleRetry();
|
|
15441
|
+
});
|
|
14312
15442
|
}
|
|
14313
15443
|
return {
|
|
14314
15444
|
report(status) {
|
|
14315
|
-
|
|
15445
|
+
desiredStatus = status;
|
|
15446
|
+
if (lastReported === status) {
|
|
15447
|
+
clearRetryTimer();
|
|
14316
15448
|
return;
|
|
14317
|
-
|
|
14318
|
-
|
|
14319
|
-
if (inFlight) {
|
|
14320
|
-
try {
|
|
14321
|
-
await inFlight;
|
|
14322
|
-
} catch {}
|
|
14323
|
-
}
|
|
14324
|
-
await post(status);
|
|
14325
|
-
};
|
|
14326
|
-
inFlight = run().finally(() => {
|
|
14327
|
-
inFlight = null;
|
|
14328
|
-
});
|
|
15449
|
+
}
|
|
15450
|
+
drain();
|
|
14329
15451
|
},
|
|
14330
15452
|
reset() {
|
|
14331
15453
|
lastReported = null;
|
|
15454
|
+
retryAttempt = 0;
|
|
15455
|
+
drain();
|
|
14332
15456
|
}
|
|
14333
15457
|
};
|
|
14334
15458
|
}
|
|
@@ -14378,6 +15502,9 @@ function createTunnelService(opts) {
|
|
|
14378
15502
|
let authRefreshTimer = null;
|
|
14379
15503
|
let authRefreshPromise = null;
|
|
14380
15504
|
let pendingAuthRefreshReason = null;
|
|
15505
|
+
const connectStatusReporter = createConnectStatusReporter({
|
|
15506
|
+
logger: opts.logger
|
|
15507
|
+
});
|
|
14381
15508
|
function emitRelayEvent(event, payload) {
|
|
14382
15509
|
if (!client?.isConnected())
|
|
14383
15510
|
return false;
|
|
@@ -14405,7 +15532,7 @@ function createTunnelService(opts) {
|
|
|
14405
15532
|
}
|
|
14406
15533
|
function readLockInfo(filePath) {
|
|
14407
15534
|
try {
|
|
14408
|
-
const parsed = JSON.parse(
|
|
15535
|
+
const parsed = JSON.parse(import_node_fs34.readFileSync(filePath, "utf-8"));
|
|
14409
15536
|
const pid = typeof parsed.pid === "number" ? parsed.pid : null;
|
|
14410
15537
|
const updatedAtMs = typeof parsed.updatedAt === "string" ? Date.parse(parsed.updatedAt) : Number.NaN;
|
|
14411
15538
|
return {
|
|
@@ -14438,7 +15565,7 @@ function createTunnelService(opts) {
|
|
|
14438
15565
|
if (!filePath)
|
|
14439
15566
|
return;
|
|
14440
15567
|
try {
|
|
14441
|
-
|
|
15568
|
+
import_node_fs34.writeFileSync(filePath, buildLockPayload(), { mode: 384 });
|
|
14442
15569
|
} catch {}
|
|
14443
15570
|
}, intervalMs);
|
|
14444
15571
|
lockRefreshTimer.unref?.();
|
|
@@ -14452,25 +15579,25 @@ function createTunnelService(opts) {
|
|
|
14452
15579
|
lockStartedAt = null;
|
|
14453
15580
|
if (fd !== null) {
|
|
14454
15581
|
try {
|
|
14455
|
-
|
|
15582
|
+
import_node_fs34.closeSync(fd);
|
|
14456
15583
|
} catch {}
|
|
14457
15584
|
}
|
|
14458
15585
|
if (filePath) {
|
|
14459
15586
|
try {
|
|
14460
|
-
|
|
15587
|
+
import_node_fs34.unlinkSync(filePath);
|
|
14461
15588
|
} catch {}
|
|
14462
15589
|
}
|
|
14463
15590
|
}
|
|
14464
15591
|
function acquireLock(filePath) {
|
|
14465
|
-
|
|
15592
|
+
import_node_fs34.mkdirSync(import_node_path31.dirname(filePath), { recursive: true });
|
|
14466
15593
|
const heartbeatSec = opts.heartbeatSec ?? DEFAULT_HEARTBEAT_SEC;
|
|
14467
15594
|
const refreshMs = heartbeatSec * 1000 * LOCK_REFRESH_HEARTBEAT_MULTIPLIER;
|
|
14468
15595
|
const staleMs = heartbeatSec * 1000 * LOCK_STALE_HEARTBEAT_MULTIPLIER;
|
|
14469
15596
|
for (let attempt = 0;attempt < 2; attempt++) {
|
|
14470
15597
|
try {
|
|
14471
15598
|
lockStartedAt = new Date().toISOString();
|
|
14472
|
-
const fd =
|
|
14473
|
-
|
|
15599
|
+
const fd = import_node_fs34.openSync(filePath, "wx", 384);
|
|
15600
|
+
import_node_fs34.writeFileSync(fd, buildLockPayload());
|
|
14474
15601
|
lockFilePath = filePath;
|
|
14475
15602
|
lockFd = fd;
|
|
14476
15603
|
startLockRefresh(refreshMs);
|
|
@@ -14488,7 +15615,7 @@ function createTunnelService(opts) {
|
|
|
14488
15615
|
const staleReason = describeStaleLock(ownerPid, updatedAt, staleMs);
|
|
14489
15616
|
opts.logger.warn(`Relay tunnel: removing stale local lock (${staleReason})`);
|
|
14490
15617
|
try {
|
|
14491
|
-
|
|
15618
|
+
import_node_fs34.unlinkSync(filePath);
|
|
14492
15619
|
} catch {}
|
|
14493
15620
|
continue;
|
|
14494
15621
|
}
|
|
@@ -14593,10 +15720,10 @@ function createTunnelService(opts) {
|
|
|
14593
15720
|
const { logger } = opts;
|
|
14594
15721
|
const hostStateDir = resolveHostStateDir(ctx.stateDir);
|
|
14595
15722
|
const stateDir = resolveStateDir(hostStateDir);
|
|
14596
|
-
const baseStateDir =
|
|
15723
|
+
const baseStateDir = import_node_path31.join(stateDir, "plugins", "phone-notifications");
|
|
14597
15724
|
logger.info(`Relay tunnel: starting (pid=${process.pid}, url=${redactUrlSecrets(opts.tunnelUrl)}, heartbeat=${opts.heartbeatSec ?? DEFAULT_HEARTBEAT_SEC}s, backoff=${opts.reconnectBackoffMs ?? DEFAULT_RECONNECT_BACKOFF_MS}ms, gateway=${opts.gatewayBaseUrl}, hasGatewayToken=${!!opts.gatewayToken}, hasGatewayPwd=${!!opts.gatewayPassword})`);
|
|
14598
|
-
const statusFilePath =
|
|
14599
|
-
const lockPath =
|
|
15725
|
+
const statusFilePath = import_node_path31.join(baseStateDir, "tunnel-status.json");
|
|
15726
|
+
const lockPath = import_node_path31.join(baseStateDir, "relay-tunnel.lock");
|
|
14600
15727
|
if (!acquireLock(lockPath)) {
|
|
14601
15728
|
return;
|
|
14602
15729
|
}
|
|
@@ -14616,20 +15743,24 @@ function createTunnelService(opts) {
|
|
|
14616
15743
|
gatewayAuthMode: opts.gatewayAuthMode,
|
|
14617
15744
|
gatewayToken: opts.gatewayToken,
|
|
14618
15745
|
gatewayPassword: opts.gatewayPassword,
|
|
15746
|
+
onGatewayConnected: () => {
|
|
15747
|
+
connectStatusReporter.report("connected");
|
|
15748
|
+
},
|
|
15749
|
+
onGatewayDisconnected: () => {
|
|
15750
|
+
connectStatusReporter.report("disconnected");
|
|
15751
|
+
},
|
|
14619
15752
|
client,
|
|
14620
15753
|
logger
|
|
14621
15754
|
});
|
|
14622
|
-
const connectStatusReporter = createConnectStatusReporter({ logger });
|
|
14623
15755
|
client.onInbound((frame) => proxy.handleFrame(frame));
|
|
14624
15756
|
client.onConnected(() => {
|
|
14625
|
-
connectStatusReporter.report("connected");
|
|
14626
15757
|
emitPendingPluginUpdate("relay connected");
|
|
14627
15758
|
});
|
|
14628
15759
|
client.onDisconnected((reason) => {
|
|
14629
|
-
logger.warn(`Relay tunnel: relay disconnected, cleaning
|
|
14630
|
-
|
|
14631
|
-
proxy?.cleanup();
|
|
15760
|
+
logger.warn(`Relay tunnel: relay disconnected, cleaning relay proxy state (${reason})`);
|
|
15761
|
+
proxy?.cleanupRelayState();
|
|
14632
15762
|
});
|
|
15763
|
+
proxy.startGatewayConnection();
|
|
14633
15764
|
abortController = new AbortController;
|
|
14634
15765
|
client.connectWithAutoReconnect(abortController.signal).catch((err2) => {
|
|
14635
15766
|
releaseLock();
|
|
@@ -14692,7 +15823,7 @@ function createTunnelService(opts) {
|
|
|
14692
15823
|
}
|
|
14693
15824
|
|
|
14694
15825
|
// src/profile/auth/base-apikey.ts
|
|
14695
|
-
function
|
|
15826
|
+
function stripBearer2(apiKey) {
|
|
14696
15827
|
return apiKey.startsWith("Bearer ") ? apiKey.slice("Bearer ".length) : apiKey;
|
|
14697
15828
|
}
|
|
14698
15829
|
|
|
@@ -14710,7 +15841,7 @@ class BaseApiKeyAuthProvider {
|
|
|
14710
15841
|
if (!apiKey) {
|
|
14711
15842
|
throw new Error(`apiKey 未设置,请先执行 ntf auth set-api-key <apiKey>(或 ${credentialsPath()})`);
|
|
14712
15843
|
}
|
|
14713
|
-
const raw =
|
|
15844
|
+
const raw = stripBearer2(apiKey);
|
|
14714
15845
|
return {
|
|
14715
15846
|
query: { apiKey: raw },
|
|
14716
15847
|
headers: { "X-Api-Key-Id": raw }
|
|
@@ -14737,11 +15868,11 @@ class ArkclawAuthProvider extends BaseApiKeyAuthProvider {
|
|
|
14737
15868
|
// src/profile/auth/jvsclaw.ts
|
|
14738
15869
|
var import_node_crypto6 = require("node:crypto");
|
|
14739
15870
|
var import_node_os3 = __toESM(require("node:os"));
|
|
14740
|
-
var
|
|
14741
|
-
var
|
|
15871
|
+
var import_node_fs35 = require("node:fs");
|
|
15872
|
+
var import_node_path32 = require("node:path");
|
|
14742
15873
|
var CLAW_CODE = "JvsClaw";
|
|
14743
|
-
var
|
|
14744
|
-
var
|
|
15874
|
+
var RETRY_BASE_MS2 = 2000;
|
|
15875
|
+
var RETRY_MAX_MS2 = 60000;
|
|
14745
15876
|
var FATAL_READY_CODES = new Set([
|
|
14746
15877
|
"54016",
|
|
14747
15878
|
"54017",
|
|
@@ -14749,7 +15880,7 @@ var FATAL_READY_CODES = new Set([
|
|
|
14749
15880
|
"54020",
|
|
14750
15881
|
"54024"
|
|
14751
15882
|
]);
|
|
14752
|
-
var JVSCLAW_LINK_SECRET_CONFIG_PATH =
|
|
15883
|
+
var JVSCLAW_LINK_SECRET_CONFIG_PATH = import_node_path32.join(DEFAULT_JVSCLAW_STATE_DIR, "openclaw.json");
|
|
14753
15884
|
var PLUGIN_ID3 = "phone-notifications";
|
|
14754
15885
|
var LINK_SECRET_WATCH_DEBOUNCE_MS = 250;
|
|
14755
15886
|
|
|
@@ -14802,7 +15933,7 @@ class JvsclawAuthProvider extends BaseApiKeyAuthProvider {
|
|
|
14802
15933
|
logger.error(`jvsclaw auth: 置换失败且不可恢复(code=${err2.code}): ${err2.message},停止轮询`);
|
|
14803
15934
|
return;
|
|
14804
15935
|
}
|
|
14805
|
-
const delayMs = Math.min(
|
|
15936
|
+
const delayMs = Math.min(RETRY_BASE_MS2 * 2 ** (attempt - 1), RETRY_MAX_MS2);
|
|
14806
15937
|
logger.warn(`jvsclaw auth: 置换失败(attempt ${attempt}): ${err2?.message ?? String(err2)},${delayMs}ms 后重试`);
|
|
14807
15938
|
const aborted = await sleep2(delayMs, abortSignal);
|
|
14808
15939
|
if (aborted)
|
|
@@ -14861,8 +15992,8 @@ class JvsclawAuthProvider extends BaseApiKeyAuthProvider {
|
|
|
14861
15992
|
watch(onChange) {
|
|
14862
15993
|
const unwatchCredentials = super.watch(onChange);
|
|
14863
15994
|
const configPath = resolveConfigPath();
|
|
14864
|
-
const configDir =
|
|
14865
|
-
const configFile =
|
|
15995
|
+
const configDir = import_node_path32.dirname(configPath);
|
|
15996
|
+
const configFile = import_node_path32.basename(configPath);
|
|
14866
15997
|
let debounceTimer = null;
|
|
14867
15998
|
let configWatcher = null;
|
|
14868
15999
|
const emitChange = () => {
|
|
@@ -14875,7 +16006,7 @@ class JvsclawAuthProvider extends BaseApiKeyAuthProvider {
|
|
|
14875
16006
|
debounceTimer.unref?.();
|
|
14876
16007
|
};
|
|
14877
16008
|
try {
|
|
14878
|
-
configWatcher =
|
|
16009
|
+
configWatcher = import_node_fs35.watch(configDir, { persistent: false }, (_event, changedName) => {
|
|
14879
16010
|
if (!changedName)
|
|
14880
16011
|
return;
|
|
14881
16012
|
const name = String(changedName);
|
|
@@ -14900,7 +16031,7 @@ function hashLinkSecret(value) {
|
|
|
14900
16031
|
function readJvsclawLinkSecretFromConfigFile(configPath = resolveConfigPath()) {
|
|
14901
16032
|
let config;
|
|
14902
16033
|
try {
|
|
14903
|
-
config = JSON.parse(
|
|
16034
|
+
config = JSON.parse(import_node_fs35.readFileSync(configPath, "utf-8"));
|
|
14904
16035
|
} catch {
|
|
14905
16036
|
return;
|
|
14906
16037
|
}
|
|
@@ -14948,17 +16079,51 @@ function resolveAuthProvider(deps) {
|
|
|
14948
16079
|
}
|
|
14949
16080
|
}
|
|
14950
16081
|
|
|
16082
|
+
// src/plugin/register-guard.ts
|
|
16083
|
+
var DEFAULT_KEY = "__default__";
|
|
16084
|
+
var wiredRuntimes = new Map;
|
|
16085
|
+
function normalizeKey(key) {
|
|
16086
|
+
return key && key.length > 0 ? key : DEFAULT_KEY;
|
|
16087
|
+
}
|
|
16088
|
+
function claimWiredRuntime(key, create) {
|
|
16089
|
+
const normalized = normalizeKey(key);
|
|
16090
|
+
const existing = wiredRuntimes.get(normalized);
|
|
16091
|
+
if (existing) {
|
|
16092
|
+
return { runtime: existing, firstAssembly: false };
|
|
16093
|
+
}
|
|
16094
|
+
const runtime = create();
|
|
16095
|
+
wiredRuntimes.set(normalized, runtime);
|
|
16096
|
+
return { runtime, firstAssembly: true };
|
|
16097
|
+
}
|
|
16098
|
+
function releaseWiredRuntime(key) {
|
|
16099
|
+
wiredRuntimes.delete(normalizeKey(key));
|
|
16100
|
+
}
|
|
16101
|
+
|
|
16102
|
+
// src/plugin/shared-storage.ts
|
|
16103
|
+
var DEFAULT_KEY2 = "__default__";
|
|
16104
|
+
var registry = new Map;
|
|
16105
|
+
function normalizeKey2(key) {
|
|
16106
|
+
return key && key.length > 0 ? key : DEFAULT_KEY2;
|
|
16107
|
+
}
|
|
16108
|
+
function publishSharedStorage(key, handles) {
|
|
16109
|
+
registry.set(normalizeKey2(key), handles);
|
|
16110
|
+
}
|
|
16111
|
+
function clearSharedStorage(key) {
|
|
16112
|
+
registry.delete(normalizeKey2(key));
|
|
16113
|
+
}
|
|
16114
|
+
function getSharedNotificationStorage(key) {
|
|
16115
|
+
return registry.get(normalizeKey2(key))?.storage ?? null;
|
|
16116
|
+
}
|
|
16117
|
+
function getSharedRecordingStorage(key) {
|
|
16118
|
+
return registry.get(normalizeKey2(key))?.recordingStorage ?? null;
|
|
16119
|
+
}
|
|
16120
|
+
function getSharedImageStorage(key) {
|
|
16121
|
+
return registry.get(normalizeKey2(key))?.imageStorage ?? null;
|
|
16122
|
+
}
|
|
16123
|
+
|
|
14951
16124
|
// src/plugin/lifecycle.ts
|
|
14952
16125
|
function registerStorageLifecycle(deps) {
|
|
14953
|
-
const {
|
|
14954
|
-
api,
|
|
14955
|
-
logger,
|
|
14956
|
-
lightRuleCtx,
|
|
14957
|
-
setStorage,
|
|
14958
|
-
setRecordingStorage,
|
|
14959
|
-
setImageStorage,
|
|
14960
|
-
onStorageReady
|
|
14961
|
-
} = deps;
|
|
16126
|
+
const { api, logger, runtime, sharedStorageKey } = deps;
|
|
14962
16127
|
let storage2 = null;
|
|
14963
16128
|
let recordingStorage = null;
|
|
14964
16129
|
let imageStorage = null;
|
|
@@ -14977,32 +16142,38 @@ function registerStorageLifecycle(deps) {
|
|
|
14977
16142
|
const resolveDisplayName = appNameMapProvider.resolveDisplayName.bind(appNameMapProvider);
|
|
14978
16143
|
storage2 = new NotificationStorage(storageDir, logger, resolveDisplayName);
|
|
14979
16144
|
await storage2.init();
|
|
14980
|
-
setStorage(storage2);
|
|
14981
16145
|
logger.info(`通知存储服务已启动: ${storageDir}`);
|
|
14982
16146
|
const recDir = resolveRecordingStorageDir(storageCtx, logger);
|
|
14983
16147
|
recordingStorage = new RecordingStorage(recDir, logger);
|
|
14984
16148
|
await recordingStorage.init();
|
|
14985
|
-
setRecordingStorage(recordingStorage);
|
|
14986
16149
|
logger.info(`录音存储服务已启动: ${recDir}`);
|
|
14987
16150
|
const imageDir = resolveImageStorageDir(storageCtx, logger);
|
|
14988
16151
|
imageStorage = new ImageStorage(imageDir, logger);
|
|
14989
16152
|
await imageStorage.init();
|
|
14990
|
-
setImageStorage(imageStorage);
|
|
14991
16153
|
logger.info(`图片存储服务已启动: ${imageDir}`);
|
|
14992
|
-
|
|
14993
|
-
|
|
14994
|
-
|
|
16154
|
+
runtime.markStorageReady({
|
|
16155
|
+
storage: storage2,
|
|
16156
|
+
recordingStorage,
|
|
16157
|
+
imageStorage,
|
|
16158
|
+
workspaceDir: ctx.workspaceDir,
|
|
16159
|
+
stateDir
|
|
16160
|
+
});
|
|
16161
|
+
publishSharedStorage(sharedStorageKey, {
|
|
16162
|
+
storage: storage2,
|
|
16163
|
+
recordingStorage,
|
|
16164
|
+
imageStorage
|
|
16165
|
+
});
|
|
14995
16166
|
},
|
|
14996
16167
|
async stop() {
|
|
16168
|
+
runtime.markStorageStopped();
|
|
16169
|
+
clearSharedStorage(sharedStorageKey);
|
|
16170
|
+
releaseWiredRuntime(sharedStorageKey);
|
|
14997
16171
|
await storage2?.close();
|
|
14998
16172
|
storage2 = null;
|
|
14999
|
-
setStorage(null);
|
|
15000
16173
|
await recordingStorage?.close();
|
|
15001
16174
|
recordingStorage = null;
|
|
15002
|
-
setRecordingStorage(null);
|
|
15003
16175
|
await imageStorage?.close();
|
|
15004
16176
|
imageStorage = null;
|
|
15005
|
-
setImageStorage(null);
|
|
15006
16177
|
appNameMapProvider?.stop();
|
|
15007
16178
|
appNameMapProvider = null;
|
|
15008
16179
|
logger.info("通知/录音/图片存储服务已停止");
|
|
@@ -15014,7 +16185,7 @@ function readHostGatewayConfig(params) {
|
|
|
15014
16185
|
let configData;
|
|
15015
16186
|
if (configPath) {
|
|
15016
16187
|
try {
|
|
15017
|
-
configData = JSON.parse(
|
|
16188
|
+
configData = JSON.parse(import_node_fs36.readFileSync(configPath, "utf-8"));
|
|
15018
16189
|
} catch (err2) {
|
|
15019
16190
|
if (err2?.code !== "ENOENT") {
|
|
15020
16191
|
params.logger.warn(`Relay tunnel: 无法读取 gateway 鉴权配置 (${configPath})`);
|
|
@@ -15094,7 +16265,20 @@ function registerRelayTunnelLifecycle(deps) {
|
|
|
15094
16265
|
logger.info(`Relay tunnel: detected ${exclusiveTunnelHint} in host gateway config, skipping relay startup to keep ingress mutually exclusive`);
|
|
15095
16266
|
return null;
|
|
15096
16267
|
}
|
|
15097
|
-
const
|
|
16268
|
+
const hostGatewayConfig = readHostGatewayConfig({
|
|
16269
|
+
stateDir: openclawDir,
|
|
16270
|
+
logger
|
|
16271
|
+
});
|
|
16272
|
+
const gatewayBaseUrl = resolveLocalGatewayBaseUrl({
|
|
16273
|
+
config: hostGatewayConfig
|
|
16274
|
+
});
|
|
16275
|
+
if (gatewayBaseUrl.warning) {
|
|
16276
|
+
logger.warn(`Relay tunnel: ${gatewayBaseUrl.warning}`);
|
|
16277
|
+
}
|
|
16278
|
+
if (gatewayBaseUrl.tlsEnabled && !gatewayBaseUrl.explicit) {
|
|
16279
|
+
logger.warn("Relay tunnel: detected gateway.tls.enabled=true; local relay proxy still uses HTTP by default. If local RPC fails, set PHONE_NOTIFICATIONS_GATEWAY_BASE_URL=https://127.0.0.1:<port> or disable gateway TLS.");
|
|
16280
|
+
}
|
|
16281
|
+
logger.info(`Relay tunnel: local gateway base URL ${gatewayBaseUrl.baseUrl} (source=${gatewayBaseUrl.source})`);
|
|
15098
16282
|
const { gatewayAuthMode, gatewayToken, gatewayPassword } = resolveLocalGatewayAuth({
|
|
15099
16283
|
stateDir: openclawDir,
|
|
15100
16284
|
logger
|
|
@@ -15105,7 +16289,7 @@ function registerRelayTunnelLifecycle(deps) {
|
|
|
15105
16289
|
authProvider,
|
|
15106
16290
|
heartbeatSec: config.relay?.heartbeatSec,
|
|
15107
16291
|
reconnectBackoffMs: config.relay?.reconnectBackoffMs,
|
|
15108
|
-
gatewayBaseUrl:
|
|
16292
|
+
gatewayBaseUrl: gatewayBaseUrl.baseUrl,
|
|
15109
16293
|
gatewayAuthMode,
|
|
15110
16294
|
gatewayToken,
|
|
15111
16295
|
gatewayPassword,
|
|
@@ -15237,8 +16421,10 @@ function registerNotificationInterfaces(deps) {
|
|
|
15237
16421
|
registerGatewayMethod,
|
|
15238
16422
|
notifyNotificationSummaryStatus,
|
|
15239
16423
|
notifyNotificationSummaryResult,
|
|
15240
|
-
tunnelService
|
|
16424
|
+
tunnelService,
|
|
16425
|
+
agentToolsOnly = false
|
|
15241
16426
|
} = deps;
|
|
16427
|
+
const registerGatewayMethodForMode = agentToolsOnly ? () => {} : registerGatewayMethod;
|
|
15242
16428
|
const summaryJobs = new Map;
|
|
15243
16429
|
function pruneSummaryJobs() {
|
|
15244
16430
|
while (summaryJobs.size > SUMMARY_JOB_HISTORY_LIMIT) {
|
|
@@ -15330,7 +16516,7 @@ function registerNotificationInterfaces(deps) {
|
|
|
15330
16516
|
function resolveSummaryNotificationsDir() {
|
|
15331
16517
|
return getStorage()?.getStorageDir() ?? summaryNotificationsDir ?? null;
|
|
15332
16518
|
}
|
|
15333
|
-
|
|
16519
|
+
registerGatewayMethodForMode("notifications.push", async ({ params, respond }) => {
|
|
15334
16520
|
const storage2 = getStorage();
|
|
15335
16521
|
if (!storage2) {
|
|
15336
16522
|
respond(false, null, {
|
|
@@ -15358,7 +16544,7 @@ function registerNotificationInterfaces(deps) {
|
|
|
15358
16544
|
logger.info(`notifications[${ingestId}]: ingest done in ${Date.now() - startMs}ms ` + `(received=${result.received} ingested=${result.ingested} ` + `dedupedById=${result.dedupedById} dedupedByContent=${result.dedupedByContent} invalid=${result.invalid})`);
|
|
15359
16545
|
respond(true, toIngestResponse(result));
|
|
15360
16546
|
});
|
|
15361
|
-
|
|
16547
|
+
registerGatewayMethodForMode("notifications.summary.start", async ({ params, respond }) => {
|
|
15362
16548
|
const notificationsDir = resolveSummaryNotificationsDir();
|
|
15363
16549
|
if (!notificationsDir) {
|
|
15364
16550
|
respond(false, null, {
|
|
@@ -15390,7 +16576,7 @@ function registerNotificationInterfaces(deps) {
|
|
|
15390
16576
|
});
|
|
15391
16577
|
});
|
|
15392
16578
|
});
|
|
15393
|
-
|
|
16579
|
+
registerGatewayMethodForMode("notifications.summary.status", async ({ params, respond }) => {
|
|
15394
16580
|
const jobId = trimToUndefined2(params?.jobId);
|
|
15395
16581
|
if (!jobId) {
|
|
15396
16582
|
respond(false, null, {
|
|
@@ -15409,7 +16595,7 @@ function registerNotificationInterfaces(deps) {
|
|
|
15409
16595
|
}
|
|
15410
16596
|
respond(true, serializeSummaryJob(state));
|
|
15411
16597
|
});
|
|
15412
|
-
|
|
16598
|
+
registerGatewayMethodForMode("notifications.summary.result", async ({ params, respond }) => {
|
|
15413
16599
|
const jobId = trimToUndefined2(params?.jobId);
|
|
15414
16600
|
if (!jobId) {
|
|
15415
16601
|
respond(false, null, {
|
|
@@ -15558,6 +16744,10 @@ function registerNotificationInterfaces(deps) {
|
|
|
15558
16744
|
}
|
|
15559
16745
|
}
|
|
15560
16746
|
});
|
|
16747
|
+
if (agentToolsOnly) {
|
|
16748
|
+
logger.info("Agent 通知工具已注册: notifications_summary");
|
|
16749
|
+
return;
|
|
16750
|
+
}
|
|
15561
16751
|
api.registerHttpRoute({
|
|
15562
16752
|
path: "/notifications",
|
|
15563
16753
|
auth: "gateway",
|
|
@@ -15991,6 +17181,76 @@ function registerImageInterfaces(deps) {
|
|
|
15991
17181
|
logger.info("HTTP 图片端点已注册: POST /images");
|
|
15992
17182
|
}
|
|
15993
17183
|
|
|
17184
|
+
// src/system/os-info.ts
|
|
17185
|
+
var import_node_fs37 = require("node:fs");
|
|
17186
|
+
var import_node_os4 = __toESM(require("node:os"));
|
|
17187
|
+
function defaultReadProcVersion() {
|
|
17188
|
+
try {
|
|
17189
|
+
return import_node_fs37.readFileSync("/proc/version", "utf-8");
|
|
17190
|
+
} catch {
|
|
17191
|
+
return null;
|
|
17192
|
+
}
|
|
17193
|
+
}
|
|
17194
|
+
function detectWsl(release, env, readProcVersion) {
|
|
17195
|
+
const distro = env.WSL_DISTRO_NAME?.trim() || null;
|
|
17196
|
+
const hasWslEnv = Boolean(env.WSL_DISTRO_NAME || env.WSL_INTEROP);
|
|
17197
|
+
const haystacks = [release, readProcVersion() ?? ""].map((value) => value.toLowerCase());
|
|
17198
|
+
const kernelHit = haystacks.some((value) => value.includes("microsoft") || value.includes("wsl"));
|
|
17199
|
+
if (!hasWslEnv && !kernelHit) {
|
|
17200
|
+
return null;
|
|
17201
|
+
}
|
|
17202
|
+
const isWsl2 = Boolean(env.WSL_INTEROP) || haystacks.some((value) => value.includes("wsl2"));
|
|
17203
|
+
const version = isWsl2 ? 2 : kernelHit || hasWslEnv ? 1 : null;
|
|
17204
|
+
return { version, distro };
|
|
17205
|
+
}
|
|
17206
|
+
function detectOsInfo(probe = {}) {
|
|
17207
|
+
const platform = probe.platform ?? import_node_os4.default.platform();
|
|
17208
|
+
const arch = probe.arch ?? import_node_os4.default.arch();
|
|
17209
|
+
const release = probe.release ?? import_node_os4.default.release();
|
|
17210
|
+
const env = probe.env ?? process.env;
|
|
17211
|
+
const readProcVersion = probe.readProcVersion ?? defaultReadProcVersion;
|
|
17212
|
+
if (platform === "win32") {
|
|
17213
|
+
return { os: "windows", isWsl: false, platform, arch, release };
|
|
17214
|
+
}
|
|
17215
|
+
if (platform === "darwin") {
|
|
17216
|
+
return { os: "macos", isWsl: false, platform, arch, release };
|
|
17217
|
+
}
|
|
17218
|
+
if (platform === "linux") {
|
|
17219
|
+
const wsl = detectWsl(release, env, readProcVersion);
|
|
17220
|
+
if (wsl) {
|
|
17221
|
+
return { os: "wsl", isWsl: true, platform, arch, release, wsl };
|
|
17222
|
+
}
|
|
17223
|
+
return { os: "linux", isWsl: false, platform, arch, release };
|
|
17224
|
+
}
|
|
17225
|
+
return { os: "unknown", isWsl: false, platform, arch, release };
|
|
17226
|
+
}
|
|
17227
|
+
|
|
17228
|
+
// src/plugin/system.ts
|
|
17229
|
+
function buildOsResponse() {
|
|
17230
|
+
return detectOsInfo();
|
|
17231
|
+
}
|
|
17232
|
+
function registerSystemInterfaces(deps) {
|
|
17233
|
+
const { api, logger, registerGatewayMethod } = deps;
|
|
17234
|
+
registerGatewayMethod("system.os", async ({ respond }) => {
|
|
17235
|
+
respond(true, buildOsResponse());
|
|
17236
|
+
});
|
|
17237
|
+
api.registerHttpRoute({
|
|
17238
|
+
path: "/system/os",
|
|
17239
|
+
auth: "gateway",
|
|
17240
|
+
async handler(req, res) {
|
|
17241
|
+
if (req.method !== "GET") {
|
|
17242
|
+
res.writeHead(405, { "Content-Type": "application/json" });
|
|
17243
|
+
res.end(JSON.stringify({ ok: false, error: "Method Not Allowed" }));
|
|
17244
|
+
return;
|
|
17245
|
+
}
|
|
17246
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
17247
|
+
res.end(JSON.stringify({ ok: true, ...buildOsResponse() }));
|
|
17248
|
+
}
|
|
17249
|
+
});
|
|
17250
|
+
logger.info("Gateway 系统方法已注册: system.os");
|
|
17251
|
+
logger.info("HTTP 系统端点已注册: GET /system/os");
|
|
17252
|
+
}
|
|
17253
|
+
|
|
15994
17254
|
// src/plugin/runtime-mode.ts
|
|
15995
17255
|
var PLUGIN_CLI_COMMANDS = new Set(["ntf", "phone-notifications"]);
|
|
15996
17256
|
var GLOBAL_OPTIONS_WITH_VALUE = [
|
|
@@ -16025,15 +17285,94 @@ function isPluginCliInvocation(argv = process.argv) {
|
|
|
16025
17285
|
return primaryCommand ? PLUGIN_CLI_COMMANDS.has(primaryCommand) : false;
|
|
16026
17286
|
}
|
|
16027
17287
|
|
|
17288
|
+
// src/plugin/runtime.ts
|
|
17289
|
+
class PluginRuntime {
|
|
17290
|
+
logger;
|
|
17291
|
+
lightRuleCtx;
|
|
17292
|
+
tunnelService = null;
|
|
17293
|
+
storage = null;
|
|
17294
|
+
recordingStorage = null;
|
|
17295
|
+
imageStorage = null;
|
|
17296
|
+
broadcastFn = null;
|
|
17297
|
+
storageReady = false;
|
|
17298
|
+
storageReadyListeners = [];
|
|
17299
|
+
broadcastChangedListeners = [];
|
|
17300
|
+
constructor(logger, stateDir) {
|
|
17301
|
+
this.logger = logger;
|
|
17302
|
+
this.lightRuleCtx = { stateDir };
|
|
17303
|
+
}
|
|
17304
|
+
getStorage() {
|
|
17305
|
+
return this.storage;
|
|
17306
|
+
}
|
|
17307
|
+
getRecordingStorage() {
|
|
17308
|
+
return this.recordingStorage;
|
|
17309
|
+
}
|
|
17310
|
+
getImageStorage() {
|
|
17311
|
+
return this.imageStorage;
|
|
17312
|
+
}
|
|
17313
|
+
getBroadcastFn() {
|
|
17314
|
+
return this.broadcastFn;
|
|
17315
|
+
}
|
|
17316
|
+
markStorageReady(params) {
|
|
17317
|
+
this.storage = params.storage;
|
|
17318
|
+
this.recordingStorage = params.recordingStorage;
|
|
17319
|
+
this.imageStorage = params.imageStorage;
|
|
17320
|
+
this.lightRuleCtx.workspaceDir = params.workspaceDir;
|
|
17321
|
+
this.lightRuleCtx.stateDir = params.stateDir;
|
|
17322
|
+
this.storageReady = true;
|
|
17323
|
+
this.emitAll(this.storageReadyListeners, "onStorageReady");
|
|
17324
|
+
}
|
|
17325
|
+
markStorageStopped() {
|
|
17326
|
+
this.storage = null;
|
|
17327
|
+
this.recordingStorage = null;
|
|
17328
|
+
this.imageStorage = null;
|
|
17329
|
+
this.storageReady = false;
|
|
17330
|
+
}
|
|
17331
|
+
onStorageReady(listener) {
|
|
17332
|
+
this.storageReadyListeners.push(listener);
|
|
17333
|
+
if (this.storageReady) {
|
|
17334
|
+
this.emitOne(listener, "onStorageReady");
|
|
17335
|
+
}
|
|
17336
|
+
}
|
|
17337
|
+
cacheBroadcast(broadcast) {
|
|
17338
|
+
if (!broadcast) {
|
|
17339
|
+
return;
|
|
17340
|
+
}
|
|
17341
|
+
const changed = this.broadcastFn !== broadcast;
|
|
17342
|
+
this.broadcastFn = broadcast;
|
|
17343
|
+
if (changed) {
|
|
17344
|
+
this.emitAll(this.broadcastChangedListeners, "onBroadcastChanged");
|
|
17345
|
+
}
|
|
17346
|
+
}
|
|
17347
|
+
onBroadcastChanged(listener) {
|
|
17348
|
+
this.broadcastChangedListeners.push(listener);
|
|
17349
|
+
if (this.broadcastFn) {
|
|
17350
|
+
this.emitOne(listener, "onBroadcastChanged");
|
|
17351
|
+
}
|
|
17352
|
+
}
|
|
17353
|
+
emitAll(listeners, name) {
|
|
17354
|
+
for (const listener of listeners) {
|
|
17355
|
+
this.emitOne(listener, name);
|
|
17356
|
+
}
|
|
17357
|
+
}
|
|
17358
|
+
emitOne(listener, name) {
|
|
17359
|
+
try {
|
|
17360
|
+
listener();
|
|
17361
|
+
} catch (err2) {
|
|
17362
|
+
this.logger.warn(`PluginRuntime: ${name} listener failed: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
17363
|
+
}
|
|
17364
|
+
}
|
|
17365
|
+
}
|
|
17366
|
+
|
|
16028
17367
|
// src/index.ts
|
|
16029
17368
|
|
|
16030
17369
|
var LIGHT_RULE_PROMPT_GUIDANCE = [
|
|
16031
|
-
"
|
|
17370
|
+
"灯效路由提示:",
|
|
16032
17371
|
`- 当用户表达“收到/当/如果/有某类通知或消息时,亮灯/闪灯/变成某种灯效”时,这是云端持久灯效规则请求,必须调用 ${LIGHT_RULE_TOOL_NAMES.create} 或 ${LIGHT_RULE_TOOL_NAMES.update}。`,
|
|
17372
|
+
"- 当用户要求现在/立即亮灯,或在当前一次性任务完成后亮灯时,必须调用 light_control;不要创建灯效规则。",
|
|
16033
17373
|
"- 创建规则时把用户的自然语言诉求整理成 ruleText;由 Notification Intelligence Service 的独立 Agent 编译和保存规则。",
|
|
16034
17374
|
"- 灯效规则的创建、修改、删除和查询都走 lightrules_* 工具;不要手写或编辑 tasks/*/meta.json。",
|
|
16035
|
-
"- 如果 lightrules_* 工具返回错误,必须如实说明本次查询/修改失败;不要使用聊天历史或旧 toolResult 里的规则列表冒充当前云端结果。"
|
|
16036
|
-
"- 插件不再提供 OpenClaw 层的一次性 light_control,下发、评估和亮灯原因由云端服务处理。"
|
|
17375
|
+
"- 如果 lightrules_* 工具返回错误,必须如实说明本次查询/修改失败;不要使用聊天历史或旧 toolResult 里的规则列表冒充当前云端结果。"
|
|
16037
17376
|
].join(`
|
|
16038
17377
|
`);
|
|
16039
17378
|
var src_default = {
|
|
@@ -16042,16 +17381,12 @@ var src_default = {
|
|
|
16042
17381
|
register(api) {
|
|
16043
17382
|
const config = api.pluginConfig ?? {};
|
|
16044
17383
|
const ignoredApps = new Set(config.ignoredApps ?? []);
|
|
16045
|
-
let storage2 = null;
|
|
16046
|
-
let recordingStorage = null;
|
|
16047
|
-
let imageStorage = null;
|
|
16048
|
-
let broadcastFn = null;
|
|
16049
|
-
let autoUpdateLifecycle = null;
|
|
16050
|
-
let tunnelService = null;
|
|
16051
17384
|
const openclawDir = resolvePluginStateDir(api);
|
|
16052
17385
|
const logger = openclawDir ? new PluginFileLogger(api.logger, openclawDir) : createVersionAwareLogger(api.logger);
|
|
17386
|
+
const registrationMode = api.registrationMode ?? "(unknown)";
|
|
17387
|
+
logger.info(`register() 调用: registrationMode=${registrationMode} cliInvocation=${isPluginCliInvocation()}`);
|
|
16053
17388
|
ensureNtfBinInstalled({
|
|
16054
|
-
ntfCjsPath:
|
|
17389
|
+
ntfCjsPath: import_node_path33.join(__dirname, "bin", "ntf.cjs"),
|
|
16055
17390
|
stateDir: openclawDir,
|
|
16056
17391
|
logger
|
|
16057
17392
|
});
|
|
@@ -16063,19 +17398,10 @@ var src_default = {
|
|
|
16063
17398
|
logger.info("检测到插件 CLI 命令执行上下文,仅注册 CLI,跳过后台服务以避免影响 Relay tunnel 常驻连接");
|
|
16064
17399
|
return;
|
|
16065
17400
|
}
|
|
17401
|
+
const { runtime, firstAssembly } = claimWiredRuntime(openclawDir, () => new PluginRuntime(logger, openclawDir));
|
|
16066
17402
|
api.on("before_prompt_build", () => ({
|
|
16067
17403
|
appendSystemContext: LIGHT_RULE_PROMPT_GUIDANCE
|
|
16068
17404
|
}));
|
|
16069
|
-
function cacheBroadcast(broadcast) {
|
|
16070
|
-
if (!broadcast) {
|
|
16071
|
-
return;
|
|
16072
|
-
}
|
|
16073
|
-
const changed = broadcastFn !== broadcast;
|
|
16074
|
-
broadcastFn = broadcast;
|
|
16075
|
-
if (changed) {
|
|
16076
|
-
autoUpdateLifecycle?.notifyBroadcastReady();
|
|
16077
|
-
}
|
|
16078
|
-
}
|
|
16079
17405
|
function isDirectMobileGatewayRequest(opts) {
|
|
16080
17406
|
const client = opts.client?.connect?.client;
|
|
16081
17407
|
if (!client)
|
|
@@ -16086,18 +17412,18 @@ var src_default = {
|
|
|
16086
17412
|
return client.id === "openclaw-ios" || client.id === "openclaw-android";
|
|
16087
17413
|
}
|
|
16088
17414
|
async function deactivateRelayForDirectGatewayRequest(method, opts) {
|
|
16089
|
-
if (!tunnelService || !isDirectMobileGatewayRequest(opts))
|
|
17415
|
+
if (!runtime.tunnelService || !isDirectMobileGatewayRequest(opts))
|
|
16090
17416
|
return;
|
|
16091
17417
|
const client = opts.client?.connect?.client;
|
|
16092
17418
|
try {
|
|
16093
|
-
await tunnelService.deactivateForExternalTunnel(`gateway method ${method} from ${client?.id ?? "unknown"}`);
|
|
17419
|
+
await runtime.tunnelService.deactivateForExternalTunnel(`gateway method ${method} from ${client?.id ?? "unknown"}`);
|
|
16094
17420
|
} catch (err2) {
|
|
16095
17421
|
logger.warn(`Relay tunnel: failed to deactivate after direct gateway method ${method}: ${err2?.message ?? String(err2)}`);
|
|
16096
17422
|
}
|
|
16097
17423
|
}
|
|
16098
17424
|
function registerGatewayMethodWithBroadcastCapture(method, handler) {
|
|
16099
17425
|
api.registerGatewayMethod(method, async (opts) => {
|
|
16100
|
-
cacheBroadcast(opts.context?.broadcast);
|
|
17426
|
+
runtime.cacheBroadcast(opts.context?.broadcast);
|
|
16101
17427
|
await deactivateRelayForDirectGatewayRequest(method, opts);
|
|
16102
17428
|
return handler(opts);
|
|
16103
17429
|
});
|
|
@@ -16106,7 +17432,8 @@ var src_default = {
|
|
|
16106
17432
|
return items.filter((notification) => !ignoredApps.has(notification.app));
|
|
16107
17433
|
}
|
|
16108
17434
|
function notifyRecordingStatus(event) {
|
|
16109
|
-
const relayEmitted = tunnelService?.emitEvent("recording.status", event) ?? false;
|
|
17435
|
+
const relayEmitted = runtime.tunnelService?.emitEvent("recording.status", event) ?? false;
|
|
17436
|
+
const broadcastFn = runtime.getBroadcastFn();
|
|
16110
17437
|
if (!broadcastFn) {
|
|
16111
17438
|
if (relayEmitted)
|
|
16112
17439
|
return;
|
|
@@ -16116,7 +17443,8 @@ var src_default = {
|
|
|
16116
17443
|
broadcastFn("recording.status", event);
|
|
16117
17444
|
}
|
|
16118
17445
|
function notifyNotificationSummaryStatus(event) {
|
|
16119
|
-
const relayEmitted = tunnelService?.emitEvent(NOTIFICATION_SUMMARY_STATUS_EVENT, event) ?? false;
|
|
17446
|
+
const relayEmitted = runtime.tunnelService?.emitEvent(NOTIFICATION_SUMMARY_STATUS_EVENT, event) ?? false;
|
|
17447
|
+
const broadcastFn = runtime.getBroadcastFn();
|
|
16120
17448
|
if (!broadcastFn) {
|
|
16121
17449
|
if (relayEmitted)
|
|
16122
17450
|
return;
|
|
@@ -16128,7 +17456,8 @@ var src_default = {
|
|
|
16128
17456
|
broadcastFn(NOTIFICATION_SUMMARY_STATUS_EVENT, event);
|
|
16129
17457
|
}
|
|
16130
17458
|
function notifyNotificationSummaryResult(event) {
|
|
16131
|
-
const relayEmitted = tunnelService?.emitEvent(NOTIFICATION_SUMMARY_RESULT_EVENT, event) ?? false;
|
|
17459
|
+
const relayEmitted = runtime.tunnelService?.emitEvent(NOTIFICATION_SUMMARY_RESULT_EVENT, event) ?? false;
|
|
17460
|
+
const broadcastFn = runtime.getBroadcastFn();
|
|
16132
17461
|
if (!broadcastFn) {
|
|
16133
17462
|
if (relayEmitted)
|
|
16134
17463
|
return;
|
|
@@ -16140,28 +17469,36 @@ var src_default = {
|
|
|
16140
17469
|
broadcastFn(NOTIFICATION_SUMMARY_RESULT_EVENT, event);
|
|
16141
17470
|
}
|
|
16142
17471
|
const lightRuleClient = new NotificationIntelligenceLightRuleClient({ logger });
|
|
16143
|
-
|
|
16144
|
-
|
|
16145
|
-
|
|
16146
|
-
|
|
17472
|
+
if (!firstAssembly) {
|
|
17473
|
+
registerNotificationInterfaces({
|
|
17474
|
+
api,
|
|
17475
|
+
logger,
|
|
17476
|
+
getStorage: () => runtime.getStorage() ?? getSharedNotificationStorage(openclawDir),
|
|
17477
|
+
summaryNotificationsDir: getStateFallbackNotificationDir(openclawDir),
|
|
17478
|
+
filterNotifications,
|
|
17479
|
+
registerGatewayMethod: () => {},
|
|
17480
|
+
notifyNotificationSummaryStatus,
|
|
17481
|
+
notifyNotificationSummaryResult,
|
|
17482
|
+
tunnelService: runtime.tunnelService,
|
|
17483
|
+
agentToolsOnly: true
|
|
17484
|
+
});
|
|
17485
|
+
registerLightControlTool(api, logger);
|
|
17486
|
+
registerLightRulesTools(api, lightRuleClient, logger);
|
|
17487
|
+
registerAutoUpdateAgentTool({ api, config, logger });
|
|
17488
|
+
logger.info(`register(): 本进程已完成后台装配(openclawDir=${openclawDir ?? "(default)"}),` + "已为当前 Agent collector 补注册工具,跳过重复后台 wiring");
|
|
17489
|
+
return;
|
|
17490
|
+
}
|
|
17491
|
+
const lightRuleRegistry = new LightRuleRegistry(runtime.lightRuleCtx);
|
|
17492
|
+
runtime.onStorageReady(() => {
|
|
17493
|
+
lightRuleRegistry.reload();
|
|
17494
|
+
});
|
|
16147
17495
|
registerStorageLifecycle({
|
|
16148
17496
|
api,
|
|
16149
17497
|
logger,
|
|
16150
|
-
|
|
16151
|
-
|
|
16152
|
-
storage2 = nextStorage;
|
|
16153
|
-
},
|
|
16154
|
-
setRecordingStorage(nextRecordingStorage) {
|
|
16155
|
-
recordingStorage = nextRecordingStorage;
|
|
16156
|
-
},
|
|
16157
|
-
setImageStorage(nextImageStorage) {
|
|
16158
|
-
imageStorage = nextImageStorage;
|
|
16159
|
-
},
|
|
16160
|
-
onStorageReady() {
|
|
16161
|
-
lightRuleRegistry.reload();
|
|
16162
|
-
}
|
|
17498
|
+
runtime,
|
|
17499
|
+
sharedStorageKey: openclawDir
|
|
16163
17500
|
});
|
|
16164
|
-
tunnelService = registerRelayTunnelLifecycle({
|
|
17501
|
+
runtime.tunnelService = registerRelayTunnelLifecycle({
|
|
16165
17502
|
api,
|
|
16166
17503
|
config,
|
|
16167
17504
|
logger,
|
|
@@ -16170,41 +17507,48 @@ var src_default = {
|
|
|
16170
17507
|
registerNotificationInterfaces({
|
|
16171
17508
|
api,
|
|
16172
17509
|
logger,
|
|
16173
|
-
getStorage: () =>
|
|
17510
|
+
getStorage: () => runtime.getStorage() ?? getSharedNotificationStorage(openclawDir),
|
|
16174
17511
|
summaryNotificationsDir: getStateFallbackNotificationDir(openclawDir),
|
|
16175
17512
|
filterNotifications,
|
|
16176
17513
|
registerGatewayMethod: registerGatewayMethodWithBroadcastCapture,
|
|
16177
17514
|
notifyNotificationSummaryStatus,
|
|
16178
17515
|
notifyNotificationSummaryResult,
|
|
16179
|
-
tunnelService
|
|
17516
|
+
tunnelService: runtime.tunnelService
|
|
16180
17517
|
});
|
|
16181
17518
|
registerRecordingInterfaces({
|
|
16182
17519
|
api,
|
|
16183
17520
|
logger,
|
|
16184
|
-
getRecordingStorage: () =>
|
|
17521
|
+
getRecordingStorage: () => runtime.getRecordingStorage() ?? getSharedRecordingStorage(openclawDir),
|
|
16185
17522
|
notifyRecordingStatus,
|
|
16186
17523
|
registerGatewayMethod: registerGatewayMethodWithBroadcastCapture,
|
|
16187
|
-
tunnelService
|
|
17524
|
+
tunnelService: runtime.tunnelService
|
|
16188
17525
|
});
|
|
16189
17526
|
registerImageInterfaces({
|
|
16190
17527
|
api,
|
|
16191
17528
|
logger,
|
|
16192
|
-
getImageStorage: () =>
|
|
17529
|
+
getImageStorage: () => runtime.getImageStorage() ?? getSharedImageStorage(openclawDir),
|
|
16193
17530
|
registerGatewayMethod: registerGatewayMethodWithBroadcastCapture,
|
|
16194
17531
|
maxBytes: config.image?.maxBytes ?? 0,
|
|
16195
|
-
tunnelService
|
|
17532
|
+
tunnelService: runtime.tunnelService
|
|
17533
|
+
});
|
|
17534
|
+
registerSystemInterfaces({
|
|
17535
|
+
api,
|
|
17536
|
+
logger,
|
|
17537
|
+
registerGatewayMethod: registerGatewayMethodWithBroadcastCapture
|
|
16196
17538
|
});
|
|
16197
|
-
|
|
17539
|
+
registerLightControlTool(api, logger);
|
|
17540
|
+
registerLightRulesGateway(api, lightRuleRegistry, logger, (broadcast) => runtime.cacheBroadcast(broadcast));
|
|
16198
17541
|
registerLightRulesTools(api, lightRuleClient, logger);
|
|
16199
17542
|
logger.info(`灯效规则方法已注册: gateway=${LIGHT_RULE_GATEWAY_METHOD_LIST.join(", ")}; tools=${LIGHT_RULE_TOOL_NAME_LIST.join(", ")}`);
|
|
16200
|
-
autoUpdateLifecycle = registerAutoUpdateLifecycle({
|
|
17543
|
+
const autoUpdateLifecycle = registerAutoUpdateLifecycle({
|
|
16201
17544
|
api,
|
|
16202
17545
|
config,
|
|
16203
17546
|
logger,
|
|
16204
|
-
getBroadcastFn: () =>
|
|
16205
|
-
cacheBroadcast,
|
|
16206
|
-
tunnelService
|
|
17547
|
+
getBroadcastFn: () => runtime.getBroadcastFn(),
|
|
17548
|
+
cacheBroadcast: (broadcast) => runtime.cacheBroadcast(broadcast),
|
|
17549
|
+
tunnelService: runtime.tunnelService
|
|
16207
17550
|
});
|
|
17551
|
+
runtime.onBroadcastChanged(() => autoUpdateLifecycle.notifyBroadcastReady());
|
|
16208
17552
|
registerPluginCli(api, {
|
|
16209
17553
|
logger,
|
|
16210
17554
|
openclawDir
|
|
@@ -16212,5 +17556,5 @@ var src_default = {
|
|
|
16212
17556
|
}
|
|
16213
17557
|
};
|
|
16214
17558
|
|
|
16215
|
-
//# debugId=
|
|
17559
|
+
//# debugId=3D7085ABD8318D3264756E2164756E21
|
|
16216
17560
|
//# sourceMappingURL=index.cjs.map
|