ylib-syim 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bridges/main.ts +3 -3
- package/bridges/runtime/bot-runtime-registry.ts +68 -0
- package/bridges/runtime/config-client.ts +24 -0
- package/bridges/runtime/http-control-server.ts +7 -0
- package/bridges/runtime/plugin-schema-service.ts +46 -0
- package/bridges/runtime/runtime-controller.ts +37 -0
- package/package.json +1 -1
package/bridges/main.ts
CHANGED
|
@@ -4,9 +4,9 @@ import http from "node:http";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { calcSchemaHash as calcSchemaHashFromRuntime } from "
|
|
8
|
-
import { createHttpControlServer } from "
|
|
9
|
-
import { pullRuntimeConfig } from "
|
|
7
|
+
import { calcSchemaHash as calcSchemaHashFromRuntime } from "./runtime/plugin-schema-service.ts";
|
|
8
|
+
import { createHttpControlServer } from "./runtime/http-control-server.ts";
|
|
9
|
+
import { pullRuntimeConfig } from "./runtime/config-client.ts";
|
|
10
10
|
|
|
11
11
|
const bridgeDir = path.dirname(fileURLToPath(import.meta.url));
|
|
12
12
|
const packageRoot = path.resolve(bridgeDir, "..");
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export type RuntimeLinkStatus =
|
|
2
|
+
| "connecting"
|
|
3
|
+
| "connected"
|
|
4
|
+
| "degraded"
|
|
5
|
+
| "disconnected"
|
|
6
|
+
| "error";
|
|
7
|
+
|
|
8
|
+
export type RuntimeBotStatus = {
|
|
9
|
+
platform: "dingtalk" | "feishu";
|
|
10
|
+
bot_account_id: string;
|
|
11
|
+
link_status: RuntimeLinkStatus;
|
|
12
|
+
started_at: string | null;
|
|
13
|
+
last_heartbeat_at: string | null;
|
|
14
|
+
last_error: string | null;
|
|
15
|
+
reconnect_count?: number;
|
|
16
|
+
last_event?: string | null;
|
|
17
|
+
status_source?: "event" | "probe" | "manual";
|
|
18
|
+
last_probe_at?: string | null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class BotRuntimeRegistry {
|
|
22
|
+
private readonly store = new Map<string, RuntimeBotStatus>();
|
|
23
|
+
|
|
24
|
+
private keyOf(platform: "dingtalk" | "feishu", accountId: string): string {
|
|
25
|
+
return `${platform}:${accountId}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
clear(): void {
|
|
29
|
+
this.store.clear();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
upsert(entry: RuntimeBotStatus): void {
|
|
33
|
+
const key = this.keyOf(entry.platform, entry.bot_account_id);
|
|
34
|
+
const prev = this.store.get(key);
|
|
35
|
+
this.store.set(key, { ...(prev || {}), ...entry });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
list(): RuntimeBotStatus[] {
|
|
39
|
+
return Array.from(this.store.values());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
summarize(): {
|
|
43
|
+
total: number;
|
|
44
|
+
connected: number;
|
|
45
|
+
error: number;
|
|
46
|
+
connecting: number;
|
|
47
|
+
degraded: number;
|
|
48
|
+
disconnected: number;
|
|
49
|
+
} {
|
|
50
|
+
const summary = {
|
|
51
|
+
total: 0,
|
|
52
|
+
connected: 0,
|
|
53
|
+
error: 0,
|
|
54
|
+
connecting: 0,
|
|
55
|
+
degraded: 0,
|
|
56
|
+
disconnected: 0,
|
|
57
|
+
};
|
|
58
|
+
for (const bot of this.store.values()) {
|
|
59
|
+
summary.total += 1;
|
|
60
|
+
if (bot.link_status === "connected") summary.connected += 1;
|
|
61
|
+
else if (bot.link_status === "error") summary.error += 1;
|
|
62
|
+
else if (bot.link_status === "connecting") summary.connecting += 1;
|
|
63
|
+
else if (bot.link_status === "degraded") summary.degraded += 1;
|
|
64
|
+
else if (bot.link_status === "disconnected") summary.disconnected += 1;
|
|
65
|
+
}
|
|
66
|
+
return summary;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export async function pullRuntimeConfig(options: {
|
|
2
|
+
url: string;
|
|
3
|
+
token: string;
|
|
4
|
+
version?: string;
|
|
5
|
+
}): Promise<{ ok: boolean; status: number; payload: Record<string, unknown> }> {
|
|
6
|
+
const reqUrl = new URL(options.url);
|
|
7
|
+
if (options.version) reqUrl.searchParams.set("version", options.version);
|
|
8
|
+
const response = await fetch(reqUrl.toString(), {
|
|
9
|
+
method: "GET",
|
|
10
|
+
headers: {
|
|
11
|
+
Accept: "application/json",
|
|
12
|
+
Authorization: `Bearer ${options.token}`,
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
const payload = (await response.json().catch(() => ({}))) as Record<
|
|
16
|
+
string,
|
|
17
|
+
unknown
|
|
18
|
+
>;
|
|
19
|
+
return {
|
|
20
|
+
ok: response.ok,
|
|
21
|
+
status: response.status,
|
|
22
|
+
payload,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export function pruneSchema(value: unknown): unknown {
|
|
4
|
+
if (value === null) return undefined;
|
|
5
|
+
if (Array.isArray(value)) {
|
|
6
|
+
const arr = value
|
|
7
|
+
.map((item) => pruneSchema(item))
|
|
8
|
+
.filter((item) => item !== undefined);
|
|
9
|
+
if (arr.length === 0) return undefined;
|
|
10
|
+
return arr;
|
|
11
|
+
}
|
|
12
|
+
if (typeof value === "object") {
|
|
13
|
+
const input = value as Record<string, unknown>;
|
|
14
|
+
const out: Record<string, unknown> = {};
|
|
15
|
+
const keys = Object.keys(input).sort();
|
|
16
|
+
for (const key of keys) {
|
|
17
|
+
const pruned = pruneSchema(input[key]);
|
|
18
|
+
if (pruned === undefined) continue;
|
|
19
|
+
out[key] = pruned;
|
|
20
|
+
}
|
|
21
|
+
if (Object.keys(out).length === 0) return undefined;
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function stableStringify(value: unknown): string {
|
|
28
|
+
if (value === null || typeof value !== "object") {
|
|
29
|
+
return JSON.stringify(value);
|
|
30
|
+
}
|
|
31
|
+
if (Array.isArray(value)) {
|
|
32
|
+
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
33
|
+
}
|
|
34
|
+
const obj = value as Record<string, unknown>;
|
|
35
|
+
const keys = Object.keys(obj).sort();
|
|
36
|
+
const pairs = keys.map(
|
|
37
|
+
(key) => `${JSON.stringify(key)}:${stableStringify(obj[key])}`,
|
|
38
|
+
);
|
|
39
|
+
return `{${pairs.join(",")}}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function calcSchemaHash(rawSchema: unknown): string {
|
|
43
|
+
const pruned = pruneSchema(rawSchema);
|
|
44
|
+
const canonical = stableStringify(pruned ?? {});
|
|
45
|
+
return crypto.createHash("sha256").update(canonical).digest("hex");
|
|
46
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { RuntimeBotStatus } from "./bot-runtime-registry";
|
|
2
|
+
|
|
3
|
+
export type RestartResult = {
|
|
4
|
+
ok: boolean;
|
|
5
|
+
status: "success" | "partial_success" | "fail" | "timeout";
|
|
6
|
+
bots: RuntimeBotStatus[];
|
|
7
|
+
summary: {
|
|
8
|
+
total: number;
|
|
9
|
+
connected: number;
|
|
10
|
+
error: number;
|
|
11
|
+
connecting: number;
|
|
12
|
+
degraded: number;
|
|
13
|
+
disconnected: number;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function evaluateRestartCompletion(input: {
|
|
18
|
+
bots: RuntimeBotStatus[];
|
|
19
|
+
summary: RestartResult["summary"];
|
|
20
|
+
}): RestartResult {
|
|
21
|
+
const { bots, summary } = input;
|
|
22
|
+
if (summary.total === 0) {
|
|
23
|
+
return { ok: true, status: "success", bots, summary };
|
|
24
|
+
}
|
|
25
|
+
const unsettled =
|
|
26
|
+
summary.connecting + summary.degraded + summary.disconnected;
|
|
27
|
+
if (unsettled > 0) {
|
|
28
|
+
return { ok: false, status: "timeout", bots, summary };
|
|
29
|
+
}
|
|
30
|
+
if (summary.error > 0 && summary.connected > 0) {
|
|
31
|
+
return { ok: true, status: "partial_success", bots, summary };
|
|
32
|
+
}
|
|
33
|
+
if (summary.error > 0 && summary.connected === 0) {
|
|
34
|
+
return { ok: false, status: "fail", bots, summary };
|
|
35
|
+
}
|
|
36
|
+
return { ok: true, status: "success", bots, summary };
|
|
37
|
+
}
|