@thclouds/openclaw-channel-taidesk 0.1.0 → 0.1.2
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 +2 -2
- package/index.ts +23 -7
- package/openclaw.plugin.json +6 -3
- package/package.json +9 -6
- package/src/api/{taidesk-outbound.ts → api.ts} +1 -1
- package/src/channel.ts +14 -8
- package/src/compat.ts +94 -0
- package/src/{config.ts → config/config.ts} +1 -1
- package/src/messaging/{process-inbound.ts → process-message.ts} +1 -1
- package/src/monitor/{taidesk-monitor.ts → monitor.ts} +4 -4
- package/src/mqtt/taidesk-mqtt-client.ts +3 -3
- package/src/types.ts +1 -1
- package/src/util/logger.ts +145 -0
- package/setup-entry.ts +0 -5
- /package/src/{config-schema.ts → config/config-schema.ts} +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# openclaw-channel-taidesk
|
|
2
2
|
|
|
3
|
-
OpenClaw 渠道插件:**Taidesk MQTT 入站**(订阅
|
|
3
|
+
OpenClaw 渠道插件:**Taidesk MQTT 入站**(订阅 **`/open` + agentId**,中间无 `/`)+ **HTTP 出站**。
|
|
4
4
|
|
|
5
5
|
## 文档
|
|
6
6
|
|
|
@@ -35,7 +35,7 @@ OpenClaw 渠道插件:**Taidesk MQTT 入站**(订阅 `/open/{agentId}`)+ *
|
|
|
35
35
|
}
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
订阅 topic:**`/open
|
|
38
|
+
订阅 topic:**`/open` + `agentId`**(例:`agentId` 为 `2014147198056767490` 时 topic 为 `/open2014147198056767490`)。入站 JSON 见契约文档中的 `agent_msg` 示例。
|
|
39
39
|
|
|
40
40
|
## 脚本
|
|
41
41
|
|
package/index.ts
CHANGED
|
@@ -1,14 +1,30 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
|
2
|
+
import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
|
|
2
3
|
|
|
3
4
|
import { taideskPlugin } from "./src/channel.js";
|
|
5
|
+
import { assertHostCompatibility } from "./src/compat.js";
|
|
6
|
+
import { TaideskChannelConfigSchema } from "./src/config/config-schema.js";
|
|
4
7
|
import { setTaideskRuntime } from "./src/runtime.js";
|
|
5
8
|
|
|
6
9
|
export { taideskPlugin, setTaideskRuntime };
|
|
7
10
|
|
|
8
|
-
export default
|
|
11
|
+
export default {
|
|
9
12
|
id: "taidesk",
|
|
10
|
-
name: "Taidesk
|
|
11
|
-
description: "Taidesk
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
name: "Taidesk",
|
|
14
|
+
description: "Taidesk channel (MQTT inbound + HTTP webhook outbound)",
|
|
15
|
+
configSchema: buildChannelConfigSchema(TaideskChannelConfigSchema),
|
|
16
|
+
register(api: OpenClawPluginApi) {
|
|
17
|
+
// Fail-fast: reject incompatible host versions before any side-effects.
|
|
18
|
+
assertHostCompatibility(api.runtime?.version);
|
|
19
|
+
|
|
20
|
+
if (api.runtime) {
|
|
21
|
+
setTaideskRuntime(api.runtime);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
api.registerChannel({ plugin: taideskPlugin });
|
|
25
|
+
|
|
26
|
+
// registrationMode exists in 2026.3.22+; skip heavy registrations in setup-only mode.
|
|
27
|
+
const mode = (api as { registrationMode?: string }).registrationMode;
|
|
28
|
+
if (mode && mode !== "full") return;
|
|
29
|
+
},
|
|
30
|
+
};
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "taidesk",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
|
|
3
|
+
"channels": ["taidesk"],
|
|
4
|
+
"configSchema": {
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"properties": {}
|
|
8
|
+
}
|
|
6
9
|
}
|
package/package.json
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thclouds/openclaw-channel-taidesk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "OpenClaw Taidesk channel plugin (MQTT inbound + HTTP outbound)",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"author": "thclouds",
|
|
6
7
|
"type": "module",
|
|
7
|
-
"main": "index.ts",
|
|
8
8
|
"files": [
|
|
9
|
+
"src/",
|
|
10
|
+
"!src/**/*.test.ts",
|
|
11
|
+
"!src/**/node_modules/",
|
|
9
12
|
"index.ts",
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"openclaw.plugin.json"
|
|
13
|
+
"openclaw.plugin.json",
|
|
14
|
+
"README.md"
|
|
13
15
|
],
|
|
14
16
|
"scripts": {
|
|
15
17
|
"type-check": "tsc -p tsconfig.json --noEmit",
|
|
18
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
16
19
|
"test": "vitest run",
|
|
17
|
-
"prepublishOnly": "npm run
|
|
20
|
+
"prepublishOnly": "npm run typecheck && npm test"
|
|
18
21
|
},
|
|
19
22
|
"publishConfig": {
|
|
20
23
|
"access": "public",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
2
2
|
|
|
3
|
-
import { resolveTaideskAccount } from "../config.js";
|
|
3
|
+
import { resolveTaideskAccount } from "../config/config.js";
|
|
4
4
|
import type { TaideskAccountConfig } from "../types.js";
|
|
5
5
|
|
|
6
6
|
function resolveTaideskWebhookUrl(config: TaideskAccountConfig): string {
|
package/src/channel.ts
CHANGED
|
@@ -2,16 +2,14 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
|
|
3
3
|
import type { ChannelGatewayContext, ChannelPlugin } from "openclaw/plugin-sdk";
|
|
4
4
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
5
|
-
import { buildChannelConfigSchema } from "openclaw/plugin-sdk/core";
|
|
6
5
|
|
|
7
|
-
import { TaideskChannelConfigSchema } from "./config-schema.js";
|
|
8
6
|
import {
|
|
9
7
|
applyTaideskAccountDefaults,
|
|
10
8
|
getTaideskConfig,
|
|
11
9
|
isTaideskAccountConfigured,
|
|
12
|
-
} from "./config.js";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
10
|
+
} from "./config/config.js";
|
|
11
|
+
import { sendTaideskText } from "./api/api.js";
|
|
12
|
+
import { monitorTaideskProvider } from "./monitor/monitor.js";
|
|
15
13
|
import type { TaideskAccountConfig } from "./types.js";
|
|
16
14
|
|
|
17
15
|
export type ResolvedTaideskAccount = {
|
|
@@ -40,9 +38,17 @@ export const taideskPlugin: ChannelPlugin<ResolvedTaideskAccount> = {
|
|
|
40
38
|
label: "Taidesk",
|
|
41
39
|
selectionLabel: "Taidesk (MQTT)",
|
|
42
40
|
docsPath: "/channels/taidesk",
|
|
43
|
-
|
|
41
|
+
docsLabel: "Taidesk",
|
|
42
|
+
blurb: "Taidesk:MQTT 入站 `/open`+agentId + HTTP 出站",
|
|
43
|
+
order: 76,
|
|
44
|
+
},
|
|
45
|
+
configSchema: {
|
|
46
|
+
schema: {
|
|
47
|
+
type: "object",
|
|
48
|
+
additionalProperties: false,
|
|
49
|
+
properties: {},
|
|
50
|
+
},
|
|
44
51
|
},
|
|
45
|
-
configSchema: buildChannelConfigSchema(TaideskChannelConfigSchema),
|
|
46
52
|
reload: { configPrefixes: ["channels.taidesk"] },
|
|
47
53
|
capabilities: {
|
|
48
54
|
chatTypes: ["direct"],
|
|
@@ -145,7 +151,7 @@ export const taideskPlugin: ChannelPlugin<ResolvedTaideskAccount> = {
|
|
|
145
151
|
if (!isTaideskAccountConfigured(account.config)) {
|
|
146
152
|
throw new Error("taidesk: mqttUrl、agentId、webhook、apiKey 为必填(且需替换默认占位 agentId/apiKey)");
|
|
147
153
|
}
|
|
148
|
-
const mon =
|
|
154
|
+
const mon = monitorTaideskProvider({
|
|
149
155
|
cfg,
|
|
150
156
|
accountId: account.accountId,
|
|
151
157
|
accountConfig: account.config,
|
package/src/compat.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime host-version compatibility check for openclaw-channel-taidesk.
|
|
3
|
+
*
|
|
4
|
+
* OpenClaw uses a date-based version format: YYYY.M.DD (e.g. 2026.3.22).
|
|
5
|
+
* This module parses that format and validates the running host is within
|
|
6
|
+
* the supported range for this plugin version.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
|
|
13
|
+
import { logger } from "./util/logger.js";
|
|
14
|
+
|
|
15
|
+
function readPluginVersion(): string {
|
|
16
|
+
try {
|
|
17
|
+
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const pkgPath = path.resolve(dir, "..", "package.json");
|
|
19
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) as { version?: string };
|
|
20
|
+
return pkg.version ?? "unknown";
|
|
21
|
+
} catch {
|
|
22
|
+
return "unknown";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Package version (from package.json); single source of truth with npm publish. */
|
|
27
|
+
export const PLUGIN_VERSION = readPluginVersion();
|
|
28
|
+
|
|
29
|
+
export const SUPPORTED_HOST_MIN = "2026.3.22";
|
|
30
|
+
|
|
31
|
+
export interface OpenClawVersion {
|
|
32
|
+
year: number;
|
|
33
|
+
month: number;
|
|
34
|
+
day: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Parse an OpenClaw date version string (e.g. "2026.3.22") into components.
|
|
39
|
+
* Returns null for unparseable strings.
|
|
40
|
+
*/
|
|
41
|
+
export function parseOpenClawVersion(version: string): OpenClawVersion | null {
|
|
42
|
+
// Strip any pre-release suffix (e.g. "2026.3.22-beta.1" -> "2026.3.22")
|
|
43
|
+
const base = version.trim().split("-")[0];
|
|
44
|
+
const parts = base.split(".");
|
|
45
|
+
if (parts.length !== 3) return null;
|
|
46
|
+
const [year, month, day] = parts.map(Number);
|
|
47
|
+
if (Number.isNaN(year) || Number.isNaN(month) || Number.isNaN(day)) return null;
|
|
48
|
+
return { year, month, day };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Compare two parsed versions. Returns -1 | 0 | 1.
|
|
53
|
+
*/
|
|
54
|
+
export function compareVersions(a: OpenClawVersion, b: OpenClawVersion): -1 | 0 | 1 {
|
|
55
|
+
for (const key of ["year", "month", "day"] as const) {
|
|
56
|
+
if (a[key] < b[key]) return -1;
|
|
57
|
+
if (a[key] > b[key]) return 1;
|
|
58
|
+
}
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check whether a host version string is >= SUPPORTED_HOST_MIN.
|
|
64
|
+
*/
|
|
65
|
+
export function isHostVersionSupported(hostVersion: string): boolean {
|
|
66
|
+
const host = parseOpenClawVersion(hostVersion);
|
|
67
|
+
if (!host) return false;
|
|
68
|
+
const min = parseOpenClawVersion(SUPPORTED_HOST_MIN)!;
|
|
69
|
+
return compareVersions(host, min) >= 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Fail-fast guard. Call at the very start of `register()` to prevent the
|
|
74
|
+
* plugin from loading on an incompatible host.
|
|
75
|
+
*
|
|
76
|
+
* @throws {Error} with a human-readable message when the host is out of range.
|
|
77
|
+
*/
|
|
78
|
+
export function assertHostCompatibility(hostVersion: string | undefined): void {
|
|
79
|
+
if (!hostVersion || hostVersion === "unknown") {
|
|
80
|
+
logger.warn(
|
|
81
|
+
`[compat] Could not determine host OpenClaw version; skipping compatibility check.`,
|
|
82
|
+
);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (isHostVersionSupported(hostVersion)) {
|
|
86
|
+
logger.info(`[compat] Host OpenClaw ${hostVersion} >= ${SUPPORTED_HOST_MIN}, OK.`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
throw new Error(
|
|
90
|
+
`@thclouds/openclaw-channel-taidesk@${PLUGIN_VERSION} requires OpenClaw >=${SUPPORTED_HOST_MIN}, ` +
|
|
91
|
+
`but found ${hostVersion}. ` +
|
|
92
|
+
`Please upgrade OpenClaw.`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
2
2
|
|
|
3
3
|
import { TAIDESK_ACCOUNT_DEFAULTS } from "./config-schema.js";
|
|
4
|
-
import type { TaideskAccountConfig } from "
|
|
4
|
+
import type { TaideskAccountConfig } from "../types.js";
|
|
5
5
|
|
|
6
6
|
export function getTaideskSection(cfg: OpenClawConfig): Record<string, unknown> {
|
|
7
7
|
const ch = cfg.channels as Record<string, unknown> | undefined;
|
|
@@ -3,7 +3,7 @@ import type { PluginRuntime } from "openclaw/plugin-sdk/core";
|
|
|
3
3
|
import { createTypingCallbacks } from "openclaw/plugin-sdk/channel-runtime";
|
|
4
4
|
import type { MsgContext } from "openclaw/plugin-sdk/reply-runtime";
|
|
5
5
|
|
|
6
|
-
import { sendTaideskText } from "../api/
|
|
6
|
+
import { sendTaideskText } from "../api/api.js";
|
|
7
7
|
import type { TaideskInboundEvent } from "../types.js";
|
|
8
8
|
|
|
9
9
|
export async function processTaideskInbound(params: {
|
|
@@ -4,7 +4,7 @@ import type { PluginRuntime } from "openclaw/plugin-sdk/core";
|
|
|
4
4
|
import { shouldSkipDedup } from "../dedup.js";
|
|
5
5
|
import { parseTaideskInboundJson } from "../inbound-handler.js";
|
|
6
6
|
import { createTaideskMqttConnection } from "../mqtt/taidesk-mqtt-client.js";
|
|
7
|
-
import { processTaideskInbound } from "../messaging/process-
|
|
7
|
+
import { processTaideskInbound } from "../messaging/process-message.js";
|
|
8
8
|
import { resolveTaideskChannelRuntime } from "../runtime.js";
|
|
9
9
|
import type { TaideskAccountConfig } from "../types.js";
|
|
10
10
|
|
|
@@ -19,9 +19,9 @@ export type MonitorTaideskOpts = {
|
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
22
|
+
* Taidesk 入站:MQTT 订阅 `/open`+agentId → 单条处理(对应微信 `monitorWeixinProvider`)。
|
|
23
23
|
*/
|
|
24
|
-
export function
|
|
24
|
+
export function monitorTaideskProvider(opts: MonitorTaideskOpts): { close: () => void } {
|
|
25
25
|
const log = opts.log ?? (() => {});
|
|
26
26
|
const errLog = opts.errLog ?? log;
|
|
27
27
|
const pwd = opts.accountConfig.mqttPassword ?? opts.accountConfig.apiKey ?? opts.accountConfig.token;
|
|
@@ -35,7 +35,7 @@ export function startTaideskMonitor(opts: MonitorTaideskOpts): { close: () => vo
|
|
|
35
35
|
abortSignal: opts.abortSignal,
|
|
36
36
|
onConnect: () => {
|
|
37
37
|
log(
|
|
38
|
-
`taidesk mqtt connected account=${opts.accountId} topic=/open
|
|
38
|
+
`taidesk mqtt connected account=${opts.accountId} topic=/open${opts.accountConfig.agentId}`,
|
|
39
39
|
);
|
|
40
40
|
},
|
|
41
41
|
onMessage: (_topic, raw) => {
|
|
@@ -14,7 +14,7 @@ export function normalizeMqttConnectUrl(mqttUrl: string): string {
|
|
|
14
14
|
export type TaideskMqttClientOptions = {
|
|
15
15
|
/** 例如 mqtt://、mqtts://、ws://、wss://(MQTT over WebSocket);https/http 会规范为 wss/ws */
|
|
16
16
|
mqttUrl: string;
|
|
17
|
-
/** 订阅 topic:`/open
|
|
17
|
+
/** 订阅 topic:`/open` + agentId(与 Taidesk Broker 一致,中间无 `/`) */
|
|
18
18
|
agentId: string;
|
|
19
19
|
mqttUsername?: string;
|
|
20
20
|
mqttPassword?: string;
|
|
@@ -27,11 +27,11 @@ export type TaideskMqttClientOptions = {
|
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* Taidesk 入站:MQTT 订阅 `/open
|
|
30
|
+
* Taidesk 入站:MQTT 订阅 `/open` + agentId(示例:`/open2014147198056767490`)。
|
|
31
31
|
* 重连由 mqtt.js 内置 `reconnectPeriod` 处理。
|
|
32
32
|
*/
|
|
33
33
|
export function createTaideskMqttConnection(opts: TaideskMqttClientOptions): { close: () => void } {
|
|
34
|
-
const topic = `/open
|
|
34
|
+
const topic = `/open${opts.agentId.trim()}`;
|
|
35
35
|
const connectUrl = normalizeMqttConnectUrl(opts.mqttUrl);
|
|
36
36
|
const client = mqtt.connect(connectUrl, {
|
|
37
37
|
username: opts.mqttUsername,
|
package/src/types.ts
CHANGED
|
@@ -16,7 +16,7 @@ export type TaideskAccountConfig = {
|
|
|
16
16
|
name?: string;
|
|
17
17
|
/** MQTT Broker,如 mqtts://host:8883、wss://host/mqtt;https:// 会在连接前规范为 wss:// */
|
|
18
18
|
mqttUrl: string;
|
|
19
|
-
/** 订阅 topic `/open
|
|
19
|
+
/** 订阅 topic 为 `/open` + 本字段(无中间斜杠,如 `/open2014147198056767490`) */
|
|
20
20
|
agentId: string;
|
|
21
21
|
/** 出站 Webhook 完整 URL;可用 `{id}` 占位,发送前替换为 agentId */
|
|
22
22
|
webhook: string;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/infra-runtime";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Plugin logger — writes JSON lines to the main openclaw log file:
|
|
9
|
+
* <tmpDir>/openclaw-YYYY-MM-DD.log
|
|
10
|
+
* Same file and format used by all other channels.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const MAIN_LOG_DIR = resolvePreferredOpenClawTmpDir();
|
|
14
|
+
const SUBSYSTEM = "gateway/channels/taidesk";
|
|
15
|
+
const RUNTIME = "node";
|
|
16
|
+
const RUNTIME_VERSION = process.versions.node;
|
|
17
|
+
const HOSTNAME = os.hostname() || "unknown";
|
|
18
|
+
const PARENT_NAMES = ["openclaw"];
|
|
19
|
+
|
|
20
|
+
/** tslog-compatible level IDs (higher = more severe). */
|
|
21
|
+
const LEVEL_IDS: Record<string, number> = {
|
|
22
|
+
TRACE: 1,
|
|
23
|
+
DEBUG: 2,
|
|
24
|
+
INFO: 3,
|
|
25
|
+
WARN: 4,
|
|
26
|
+
ERROR: 5,
|
|
27
|
+
FATAL: 6,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const DEFAULT_LOG_LEVEL = "INFO";
|
|
31
|
+
|
|
32
|
+
function resolveMinLevel(): number {
|
|
33
|
+
const env = process.env.OPENCLAW_LOG_LEVEL?.toUpperCase();
|
|
34
|
+
if (env && env in LEVEL_IDS) return LEVEL_IDS[env];
|
|
35
|
+
return LEVEL_IDS[DEFAULT_LOG_LEVEL];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let minLevelId = resolveMinLevel();
|
|
39
|
+
|
|
40
|
+
/** Dynamically change the minimum log level at runtime. */
|
|
41
|
+
export function setLogLevel(level: string): void {
|
|
42
|
+
const upper = level.toUpperCase();
|
|
43
|
+
if (!(upper in LEVEL_IDS)) {
|
|
44
|
+
throw new Error(`Invalid log level: ${level}. Valid levels: ${Object.keys(LEVEL_IDS).join(", ")}`);
|
|
45
|
+
}
|
|
46
|
+
minLevelId = LEVEL_IDS[upper];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Shift a Date into local time so toISOString() renders local clock digits. */
|
|
50
|
+
function toLocalISO(now: Date): string {
|
|
51
|
+
const offsetMs = -now.getTimezoneOffset() * 60_000;
|
|
52
|
+
const sign = offsetMs >= 0 ? "+" : "-";
|
|
53
|
+
const abs = Math.abs(now.getTimezoneOffset());
|
|
54
|
+
const offStr = `${sign}${String(Math.floor(abs / 60)).padStart(2, "0")}:${String(abs % 60).padStart(2, "0")}`;
|
|
55
|
+
return new Date(now.getTime() + offsetMs).toISOString().replace("Z", offStr);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function localDateKey(now: Date): string {
|
|
59
|
+
return toLocalISO(now).slice(0, 10);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveMainLogPath(): string {
|
|
63
|
+
const dateKey = localDateKey(new Date());
|
|
64
|
+
return path.join(MAIN_LOG_DIR, `openclaw-${dateKey}.log`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let logDirEnsured = false;
|
|
68
|
+
|
|
69
|
+
export type Logger = {
|
|
70
|
+
info(message: string): void;
|
|
71
|
+
debug(message: string): void;
|
|
72
|
+
warn(message: string): void;
|
|
73
|
+
error(message: string): void;
|
|
74
|
+
/** Returns a child logger whose messages are prefixed with `[accountId]`. */
|
|
75
|
+
withAccount(accountId: string): Logger;
|
|
76
|
+
/** Returns the current main log file path. */
|
|
77
|
+
getLogFilePath(): string;
|
|
78
|
+
close(): void;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
function buildLoggerName(accountId?: string): string {
|
|
82
|
+
return accountId ? `${SUBSYSTEM}/${accountId}` : SUBSYSTEM;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function writeLog(level: string, message: string, accountId?: string): void {
|
|
86
|
+
const levelId = LEVEL_IDS[level] ?? LEVEL_IDS.INFO;
|
|
87
|
+
if (levelId < minLevelId) return;
|
|
88
|
+
|
|
89
|
+
const now = new Date();
|
|
90
|
+
const loggerName = buildLoggerName(accountId);
|
|
91
|
+
const prefixedMessage = accountId ? `[${accountId}] ${message}` : message;
|
|
92
|
+
const entry = JSON.stringify({
|
|
93
|
+
"0": loggerName,
|
|
94
|
+
"1": prefixedMessage,
|
|
95
|
+
_meta: {
|
|
96
|
+
runtime: RUNTIME,
|
|
97
|
+
runtimeVersion: RUNTIME_VERSION,
|
|
98
|
+
hostname: HOSTNAME,
|
|
99
|
+
name: loggerName,
|
|
100
|
+
parentNames: PARENT_NAMES,
|
|
101
|
+
date: now.toISOString(),
|
|
102
|
+
logLevelId: LEVEL_IDS[level] ?? LEVEL_IDS.INFO,
|
|
103
|
+
logLevelName: level,
|
|
104
|
+
},
|
|
105
|
+
time: toLocalISO(now),
|
|
106
|
+
});
|
|
107
|
+
try {
|
|
108
|
+
if (!logDirEnsured) {
|
|
109
|
+
fs.mkdirSync(MAIN_LOG_DIR, { recursive: true });
|
|
110
|
+
logDirEnsured = true;
|
|
111
|
+
}
|
|
112
|
+
fs.appendFileSync(resolveMainLogPath(), `${entry}\n`, "utf-8");
|
|
113
|
+
} catch {
|
|
114
|
+
// Best-effort; never block on logging failures.
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Creates a logger instance, optionally bound to a specific account. */
|
|
119
|
+
function createLogger(accountId?: string): Logger {
|
|
120
|
+
return {
|
|
121
|
+
info(message: string): void {
|
|
122
|
+
writeLog("INFO", message, accountId);
|
|
123
|
+
},
|
|
124
|
+
debug(message: string): void {
|
|
125
|
+
writeLog("DEBUG", message, accountId);
|
|
126
|
+
},
|
|
127
|
+
warn(message: string): void {
|
|
128
|
+
writeLog("WARN", message, accountId);
|
|
129
|
+
},
|
|
130
|
+
error(message: string): void {
|
|
131
|
+
writeLog("ERROR", message, accountId);
|
|
132
|
+
},
|
|
133
|
+
withAccount(id: string): Logger {
|
|
134
|
+
return createLogger(id);
|
|
135
|
+
},
|
|
136
|
+
getLogFilePath(): string {
|
|
137
|
+
return resolveMainLogPath();
|
|
138
|
+
},
|
|
139
|
+
close(): void {
|
|
140
|
+
// No-op: appendFileSync has no persistent handle to close.
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const logger: Logger = createLogger();
|
package/setup-entry.ts
DELETED
|
File without changes
|