From b995fb94b110a0fa6c11a8ba12546ed7ffc090e7 Mon Sep 17 00:00:00 2001 From: git Date: Mon, 29 Jun 2026 15:24:20 +0800 Subject: [PATCH 1/6] Add Tencent QQ Bot OpenAPI v2 chat service --- services/bin/octobus-tentacles.js | 4 + services/bin/tencent-qq-chat.js | 12 + services/package.json | 3 + services/tencent__qq-chat/README.md | 68 ++ .../tencent__qq-chat/bin/tencent-qq-chat.js | 9 + services/tencent__qq-chat/config.schema.json | 100 ++ services/tencent__qq-chat/package.json | 12 + .../proto/tencent_qq_chat.proto | 144 +++ services/tencent__qq-chat/secret.schema.json | 39 + services/tencent__qq-chat/service.json | 61 ++ services/tencent__qq-chat/src/service.js | 7 + .../tencent__qq-chat/src/tencent-qq-chat.js | 868 ++++++++++++++++++ .../test/tencent-qq-chat.test.js | 408 ++++++++ 13 files changed, 1735 insertions(+) create mode 100755 services/bin/tencent-qq-chat.js create mode 100644 services/tencent__qq-chat/README.md create mode 100755 services/tencent__qq-chat/bin/tencent-qq-chat.js create mode 100644 services/tencent__qq-chat/config.schema.json create mode 100644 services/tencent__qq-chat/package.json create mode 100644 services/tencent__qq-chat/proto/tencent_qq_chat.proto create mode 100644 services/tencent__qq-chat/secret.schema.json create mode 100644 services/tencent__qq-chat/service.json create mode 100644 services/tencent__qq-chat/src/service.js create mode 100644 services/tencent__qq-chat/src/tencent-qq-chat.js create mode 100644 services/tencent__qq-chat/test/tencent-qq-chat.test.js diff --git a/services/bin/octobus-tentacles.js b/services/bin/octobus-tentacles.js index 6138109e..1c2ddec9 100755 --- a/services/bin/octobus-tentacles.js +++ b/services/bin/octobus-tentacles.js @@ -133,6 +133,10 @@ const services = { entryFile: "../skycloud__inet/bin/skycloud-inet.js", serviceModule: "../skycloud__inet/src/service.js", }, + "tencent-qq-chat": { + entryFile: "../tencent__qq-chat/bin/tencent-qq-chat.js", + serviceModule: "../tencent__qq-chat/src/service.js", + }, "tencent-qyweixin-group-robot": { entryFile: "../tencent__qyweixin-group-robot/bin/tencent-qyweixin-group-robot.js", serviceModule: "../tencent__qyweixin-group-robot/src/service.js", diff --git a/services/bin/tencent-qq-chat.js b/services/bin/tencent-qq-chat.js new file mode 100755 index 00000000..b0fc27b1 --- /dev/null +++ b/services/bin/tencent-qq-chat.js @@ -0,0 +1,12 @@ +#!/usr/bin/env node + +import { fileURLToPath } from "node:url"; +import { runServiceMain } from "@chaitin-ai/octobus-sdk"; + +import { service } from "../tencent__qq-chat/src/service.js"; +import { maybeAutoStartGatewayFromCli } from "../tencent__qq-chat/src/tencent-qq-chat.js"; + +await maybeAutoStartGatewayFromCli(); +runServiceMain(service, { + entryFile: fileURLToPath(new URL("../tencent__qq-chat/bin/tencent-qq-chat.js", import.meta.url)), +}); diff --git a/services/package.json b/services/package.json index dd6a1015..f949fef0 100644 --- a/services/package.json +++ b/services/package.json @@ -38,6 +38,7 @@ "skycloud-inet": "bin/skycloud-inet.js", "slack-group-robot": "bin/slack-group-robot.js", "tencent-qyweixin-group-robot": "bin/tencent-qyweixin-group-robot.js", + "tencent-qq-chat": "bin/tencent-qq-chat.js", "tencent-tsec-v2-5-1": "bin/tencent-tsec-v2-5-1.js", "threatbook-cloudapi-v3": "bin/threatbook-cloudapi-v3.js", "threatbook-claudsandbox-v3": "bin/threatbook-claudsandbox-v3.js", @@ -85,6 +86,7 @@ "bin/safeline-waf.js", "bin/sangfor-fw-v8-0-45.js", "bin/sangfor-xdr-v2-0-45.js", + "bin/tencent-qq-chat.js", "bin/tencent-qyweixin-group-robot.js", "bin/tencent-tsec-v2-5-1.js", "bin/threatbook-cloudapi-v3.js", @@ -131,6 +133,7 @@ "chaitin__safeline-waf", "sangfor__fw_v8-0-45", "sangfor__xdr_v2-0-45", + "tencent__qq-chat", "tencent__qyweixin-group-robot", "tencent__tsec_v2-5-1", "threatbook__cloudapi_v3", diff --git a/services/tencent__qq-chat/README.md b/services/tencent__qq-chat/README.md new file mode 100644 index 00000000..cc2ac251 --- /dev/null +++ b/services/tencent__qq-chat/README.md @@ -0,0 +1,68 @@ +# Tencent QQ Chat + +This OctoBus service connects to QQ chat through QQ Bot OpenAPI v2. + +It covers the stable HTTP OpenAPI side: + +- fetch `AccessToken` from `https://bots.qq.com/app/getAppAccessToken` +- send C2C messages with `/v2/users/{openid}/messages` +- send group messages with `/v2/groups/{group_openid}/messages` +- keep a long-running WebSocket Gateway receive loop for C2C/group message events +- buffer incoming messages in memory for `PollMessages` +- normalize incoming webhook/websocket payloads for C2C, group, and channel message events + +The Gateway receiver subscribes to `GROUP_AND_C2C_EVENT` by default (`1 << 25`), which covers `C2C_MESSAGE_CREATE` and `GROUP_AT_MESSAGE_CREATE`. + +## Configuration + +```json +{ + "baseUrl": "https://api.sgroup.qq.com", + "tokenUrl": "https://bots.qq.com/app/getAppAccessToken", + "timeoutMs": 5000, + "autoStartGateway": true, + "gatewayIntents": 33554432, + "maxBufferedMessages": 1000, + "gatewayReconnectMs": 5000 +} +``` + +Secrets: + +```json +{ + "appId": "APPID", + "appSecret": "APPSECRET" +} +``` + +You may provide a pre-fetched `accessToken` instead of `appId` and `appSecret`. + +## Methods + +- `GetAccessToken`: fetches or returns the configured access token. +- `StartGateway`: starts the QQ Bot WebSocket Gateway receive loop. +- `StopGateway`: stops the Gateway receive loop. +- `GetGatewayStatus`: returns Gateway connection state and buffered message counts. +- `PollMessages`: returns buffered received message events. Set `ack` to remove returned messages immediately. +- `AckMessage`: removes buffered messages by `local_id`, or all messages when `all` is true. +- `SendC2CMessage`: sends a QQ single-chat message to `openid`. +- `SendGroupMessage`: sends a QQ group message to `group_openid`. +- `NormalizeEvent`: parses official QQ Bot payload JSON and returns a stable message shape. + +For text messages, leave `msg_type` as `0` and pass `content`. For markdown, ark, keyboard, media, or message reference fields, pass the corresponding `*_json` field as a JSON object string. + +## Receiving messages + +For production instances, set `autoStartGateway` to `true` so OctoBus starts the receive loop together with the long-running service process. You can also call `StartGateway` manually after instance start. + +Incoming C2C and group-at messages are stored in an in-memory queue: + +```json +{ + "max_messages": 10, + "ack": false +} +``` + +Call `PollMessages` to read messages, then call `AckMessage` with the returned `local_id` values after they are handled. The queue is process-local; restarting the service clears unacknowledged buffered messages. diff --git a/services/tencent__qq-chat/bin/tencent-qq-chat.js b/services/tencent__qq-chat/bin/tencent-qq-chat.js new file mode 100755 index 00000000..b87939f4 --- /dev/null +++ b/services/tencent__qq-chat/bin/tencent-qq-chat.js @@ -0,0 +1,9 @@ +#!/usr/bin/env node + +import { runServiceMain } from "@chaitin-ai/octobus-sdk"; + +import { service } from "../src/service.js"; +import { maybeAutoStartGatewayFromCli } from "../src/tencent-qq-chat.js"; + +await maybeAutoStartGatewayFromCli(); +runServiceMain(service); diff --git a/services/tencent__qq-chat/config.schema.json b/services/tencent__qq-chat/config.schema.json new file mode 100644 index 00000000..32f00534 --- /dev/null +++ b/services/tencent__qq-chat/config.schema.json @@ -0,0 +1,100 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": true, + "properties": { + "baseUrl": { + "type": "string", + "default": "https://api.sgroup.qq.com", + "description": "QQ Bot OpenAPI base URL." + }, + "base_url": { + "type": "string", + "description": "Alias for baseUrl." + }, + "tokenUrl": { + "type": "string", + "default": "https://bots.qq.com/app/getAppAccessToken", + "description": "QQ Bot access-token endpoint." + }, + "token_url": { + "type": "string", + "description": "Alias for tokenUrl." + }, + "timeoutMs": { + "type": "integer", + "minimum": 1, + "default": 5000, + "description": "HTTP timeout in milliseconds." + }, + "timeout_ms": { + "type": "integer", + "minimum": 1, + "description": "Alias for timeoutMs." + }, + "autoStartGateway": { + "type": "boolean", + "default": false, + "description": "Start the QQ Bot WebSocket Gateway receiver when the long-running service process starts." + }, + "auto_start_gateway": { + "type": "boolean", + "description": "Alias for autoStartGateway." + }, + "gatewayIntents": { + "type": "integer", + "minimum": 1, + "default": 33554432, + "description": "Gateway intents bitmask. Default is GROUP_AND_C2C_EVENT (1 << 25)." + }, + "gateway_intents": { + "type": "integer", + "minimum": 1, + "description": "Alias for gatewayIntents." + }, + "gatewayShardIndex": { + "type": "integer", + "minimum": 0, + "default": 0, + "description": "Gateway shard index." + }, + "gateway_shard_index": { + "type": "integer", + "minimum": 0, + "description": "Alias for gatewayShardIndex." + }, + "gatewayShardCount": { + "type": "integer", + "minimum": 1, + "default": 1, + "description": "Gateway shard count." + }, + "gateway_shard_count": { + "type": "integer", + "minimum": 1, + "description": "Alias for gatewayShardCount." + }, + "maxBufferedMessages": { + "type": "integer", + "minimum": 1, + "default": 1000, + "description": "Maximum number of received Gateway message events kept in memory." + }, + "max_buffered_messages": { + "type": "integer", + "minimum": 1, + "description": "Alias for maxBufferedMessages." + }, + "gatewayReconnectMs": { + "type": "integer", + "minimum": 100, + "default": 5000, + "description": "Reconnect delay after Gateway disconnects." + }, + "gateway_reconnect_ms": { + "type": "integer", + "minimum": 100, + "description": "Alias for gatewayReconnectMs." + } + } +} diff --git a/services/tencent__qq-chat/package.json b/services/tencent__qq-chat/package.json new file mode 100644 index 00000000..62317406 --- /dev/null +++ b/services/tencent__qq-chat/package.json @@ -0,0 +1,12 @@ +{ + "name": "tencent-qq-chat", + "version": "0.0.0", + "private": true, + "type": "module", + "bin": { + "tencent-qq-chat": "bin/tencent-qq-chat.js" + }, + "dependencies": { + "@chaitin-ai/octobus-sdk": "^0.5.0" + } +} diff --git a/services/tencent__qq-chat/proto/tencent_qq_chat.proto b/services/tencent__qq-chat/proto/tencent_qq_chat.proto new file mode 100644 index 00000000..0f38ab8a --- /dev/null +++ b/services/tencent__qq-chat/proto/tencent_qq_chat.proto @@ -0,0 +1,144 @@ +syntax = "proto3"; + +package Tencent_QQ_Chat; + +option go_package = "miner/grpc-service/Tencent_QQ_Chat"; + +// Tencent_QQ_Chat wraps QQ Bot OpenAPI v2 chat messaging. +service Tencent_QQ_Chat { + rpc GetAccessToken(GetAccessTokenRequest) returns (GetAccessTokenResponse) {} + rpc StartGateway(StartGatewayRequest) returns (GatewayStatus) {} + rpc StopGateway(StopGatewayRequest) returns (GatewayStatus) {} + rpc GetGatewayStatus(GetGatewayStatusRequest) returns (GatewayStatus) {} + rpc PollMessages(PollMessagesRequest) returns (PollMessagesResponse) {} + rpc AckMessage(AckMessageRequest) returns (AckMessageResponse) {} + rpc SendC2CMessage(SendC2CMessageRequest) returns (SendMessageResponse) {} + rpc SendGroupMessage(SendGroupMessageRequest) returns (SendMessageResponse) {} + rpc NormalizeEvent(NormalizeEventRequest) returns (NormalizeEventResponse) {} +} + +message GetAccessTokenRequest { + bool force_refresh = 1; +} + +message GetAccessTokenResponse { + bool success = 1; + int32 http_status = 2; + string access_token = 3; + int32 expires_in = 4; + string http_body = 5; +} + +message StartGatewayRequest { + bool force_restart = 1; +} + +message StopGatewayRequest {} + +message GetGatewayStatusRequest {} + +message GatewayStatus { + bool running = 1; + bool ready = 2; + string state = 3; + string session_id = 4; + string gateway_url = 5; + string last_event_type = 6; + string last_error = 7; + string started_at = 8; + string last_event_at = 9; + string last_heartbeat_ack_at = 10; + int32 queue_size = 11; + int32 dropped_messages = 12; + int32 reconnect_count = 13; + int32 intents = 14; +} + +message PollMessagesRequest { + int32 max_messages = 1; + bool ack = 2; +} + +message ReceivedMessage { + string local_id = 1; + string received_at = 2; + NormalizeEventResponse event = 3; +} + +message PollMessagesResponse { + repeated ReceivedMessage messages = 1; + int32 queue_size = 2; +} + +message AckMessageRequest { + repeated string local_id = 1; + bool all = 2; +} + +message AckMessageResponse { + int32 acked = 1; + int32 queue_size = 2; +} + +message SendC2CMessageRequest { + string openid = 1; + string content = 2; + int32 msg_type = 3; + string markdown_json = 4; + string keyboard_json = 5; + string ark_json = 6; + string media_json = 7; + string message_reference_json = 8; + string event_id = 9; + string msg_id = 10; + int32 msg_seq = 11; + bool is_wakeup = 12; +} + +message SendGroupMessageRequest { + string group_openid = 1; + string content = 2; + int32 msg_type = 3; + string markdown_json = 4; + string keyboard_json = 5; + string ark_json = 6; + string media_json = 7; + string message_reference_json = 8; + string event_id = 9; + string msg_id = 10; + int32 msg_seq = 11; +} + +message SendMessageResponse { + bool success = 1; + int32 http_status = 2; + string id = 3; + string timestamp = 4; + string http_body = 5; +} + +message NormalizeEventRequest { + string payload_json = 1; +} + +message NormalizeEventResponse { + bool is_message = 1; + bool is_c2c = 2; + bool is_group = 3; + bool is_channel = 4; + string event_id = 5; + int32 op = 6; + string event_type = 7; + string seq = 8; + string message_id = 9; + string content = 10; + string timestamp = 11; + string openid = 12; + string group_openid = 13; + string channel_id = 14; + string guild_id = 15; + string author_json = 16; + string attachments_json = 17; + string data_json = 18; + string payload_json = 19; +} diff --git a/services/tencent__qq-chat/secret.schema.json b/services/tencent__qq-chat/secret.schema.json new file mode 100644 index 00000000..0aa440ff --- /dev/null +++ b/services/tencent__qq-chat/secret.schema.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": true, + "properties": { + "appId": { + "type": "string", + "description": "QQ Bot AppID." + }, + "app_id": { + "type": "string", + "description": "Alias for appId." + }, + "appSecret": { + "type": "string", + "description": "QQ Bot AppSecret." + }, + "app_secret": { + "type": "string", + "description": "Alias for appSecret." + }, + "clientSecret": { + "type": "string", + "description": "Alias for appSecret." + }, + "client_secret": { + "type": "string", + "description": "Alias for appSecret." + }, + "accessToken": { + "type": "string", + "description": "Pre-fetched AccessToken. If omitted, appId and appSecret are used to fetch one." + }, + "access_token": { + "type": "string", + "description": "Alias for accessToken." + } + } +} diff --git a/services/tencent__qq-chat/service.json b/services/tencent__qq-chat/service.json new file mode 100644 index 00000000..84d6806c --- /dev/null +++ b/services/tencent__qq-chat/service.json @@ -0,0 +1,61 @@ +{ + "schema": "chaitin.octobus.service.v1", + "name": "tencent-qq-chat", + "displayName": "Tencent QQ Chat", + "description": "QQ Bot OpenAPI v2 service for sending C2C/group messages and normalizing incoming chat events.", + "runtime": { + "mode": "long-running" + }, + "proto": { + "roots": [ + "proto" + ], + "files": [ + "proto/tencent_qq_chat.proto" + ] + }, + "configSchema": "config.schema.json", + "secretSchema": "secret.schema.json", + "sdk": { + "cli": { + "commands": { + "Tencent_QQ_Chat.Tencent_QQ_Chat/GetAccessToken": { + "name": "get-access-token", + "description": "Fetch a QQ Bot OpenAPI access token." + }, + "Tencent_QQ_Chat.Tencent_QQ_Chat/StartGateway": { + "name": "start-gateway", + "description": "Start the QQ Bot WebSocket Gateway receive loop." + }, + "Tencent_QQ_Chat.Tencent_QQ_Chat/StopGateway": { + "name": "stop-gateway", + "description": "Stop the QQ Bot WebSocket Gateway receive loop." + }, + "Tencent_QQ_Chat.Tencent_QQ_Chat/GetGatewayStatus": { + "name": "get-gateway-status", + "description": "Return Gateway receiver status and buffered message counts." + }, + "Tencent_QQ_Chat.Tencent_QQ_Chat/PollMessages": { + "name": "poll-messages", + "description": "Poll received QQ message events buffered by the Gateway receiver." + }, + "Tencent_QQ_Chat.Tencent_QQ_Chat/AckMessage": { + "name": "ack-message", + "description": "Acknowledge and remove buffered received messages." + }, + "Tencent_QQ_Chat.Tencent_QQ_Chat/SendC2CMessage": { + "name": "send-c2c-message", + "description": "Send a QQ C2C message through QQ Bot OpenAPI." + }, + "Tencent_QQ_Chat.Tencent_QQ_Chat/SendGroupMessage": { + "name": "send-group-message", + "description": "Send a QQ group message through QQ Bot OpenAPI." + }, + "Tencent_QQ_Chat.Tencent_QQ_Chat/NormalizeEvent": { + "name": "normalize-event", + "description": "Normalize an incoming QQ Bot event payload." + } + } + } + } +} diff --git a/services/tencent__qq-chat/src/service.js b/services/tencent__qq-chat/src/service.js new file mode 100644 index 00000000..0ae79327 --- /dev/null +++ b/services/tencent__qq-chat/src/service.js @@ -0,0 +1,7 @@ +import { defineService } from "@chaitin-ai/octobus-sdk"; + +import { handlers } from "./tencent-qq-chat.js"; + +export { handlers } from "./tencent-qq-chat.js"; + +export const service = defineService({ handlers }); diff --git a/services/tencent__qq-chat/src/tencent-qq-chat.js b/services/tencent__qq-chat/src/tencent-qq-chat.js new file mode 100644 index 00000000..0a441042 --- /dev/null +++ b/services/tencent__qq-chat/src/tencent-qq-chat.js @@ -0,0 +1,868 @@ +import fs from "node:fs"; +import { GrpcError, grpcStatus } from "@chaitin-ai/octobus-sdk"; + +export const METHOD_GET_ACCESS_TOKEN_PATH = "/Tencent_QQ_Chat.Tencent_QQ_Chat/GetAccessToken"; +export const METHOD_START_GATEWAY_PATH = "/Tencent_QQ_Chat.Tencent_QQ_Chat/StartGateway"; +export const METHOD_STOP_GATEWAY_PATH = "/Tencent_QQ_Chat.Tencent_QQ_Chat/StopGateway"; +export const METHOD_GET_GATEWAY_STATUS_PATH = "/Tencent_QQ_Chat.Tencent_QQ_Chat/GetGatewayStatus"; +export const METHOD_POLL_MESSAGES_PATH = "/Tencent_QQ_Chat.Tencent_QQ_Chat/PollMessages"; +export const METHOD_ACK_MESSAGE_PATH = "/Tencent_QQ_Chat.Tencent_QQ_Chat/AckMessage"; +export const METHOD_SEND_C2C_MESSAGE_PATH = "/Tencent_QQ_Chat.Tencent_QQ_Chat/SendC2CMessage"; +export const METHOD_SEND_GROUP_MESSAGE_PATH = "/Tencent_QQ_Chat.Tencent_QQ_Chat/SendGroupMessage"; +export const METHOD_NORMALIZE_EVENT_PATH = "/Tencent_QQ_Chat.Tencent_QQ_Chat/NormalizeEvent"; + +export const METHOD_GET_ACCESS_TOKEN_FULL = "Tencent_QQ_Chat.Tencent_QQ_Chat/GetAccessToken"; +export const METHOD_START_GATEWAY_FULL = "Tencent_QQ_Chat.Tencent_QQ_Chat/StartGateway"; +export const METHOD_STOP_GATEWAY_FULL = "Tencent_QQ_Chat.Tencent_QQ_Chat/StopGateway"; +export const METHOD_GET_GATEWAY_STATUS_FULL = "Tencent_QQ_Chat.Tencent_QQ_Chat/GetGatewayStatus"; +export const METHOD_POLL_MESSAGES_FULL = "Tencent_QQ_Chat.Tencent_QQ_Chat/PollMessages"; +export const METHOD_ACK_MESSAGE_FULL = "Tencent_QQ_Chat.Tencent_QQ_Chat/AckMessage"; +export const METHOD_SEND_C2C_MESSAGE_FULL = "Tencent_QQ_Chat.Tencent_QQ_Chat/SendC2CMessage"; +export const METHOD_SEND_GROUP_MESSAGE_FULL = "Tencent_QQ_Chat.Tencent_QQ_Chat/SendGroupMessage"; +export const METHOD_NORMALIZE_EVENT_FULL = "Tencent_QQ_Chat.Tencent_QQ_Chat/NormalizeEvent"; + +export const DEFAULT_API_BASE_URL = "https://api.sgroup.qq.com"; +export const DEFAULT_TOKEN_URL = "https://bots.qq.com/app/getAppAccessToken"; +export const DEFAULT_TIMEOUT_MS = 5000; +export const DEFAULT_GATEWAY_INTENTS = 1 << 25; +export const DEFAULT_GATEWAY_RECONNECT_MS = 5000; +export const DEFAULT_MAX_BUFFERED_MESSAGES = 1000; + +const tokenCache = new Map(); + +const gatewayState = { + running: false, + ready: false, + state: "stopped", + sessionId: "", + gatewayUrl: "", + lastEventType: "", + lastError: "", + startedAt: "", + lastEventAt: "", + lastHeartbeatAckAt: "", + queue: [], + droppedMessages: 0, + reconnectCount: 0, + intents: DEFAULT_GATEWAY_INTENTS, + seq: null, + ws: null, + heartbeatTimer: null, + reconnectTimer: null, + ctx: null, + localMessageSeq: 0, +}; + +const grpcCodeFor = (code) => ({ + FAILED_PRECONDITION: grpcStatus.FAILED_PRECONDITION, + INVALID_ARGUMENT: grpcStatus.INVALID_ARGUMENT, + PERMISSION_DENIED: grpcStatus.PERMISSION_DENIED, + UNAUTHENTICATED: grpcStatus.UNAUTHENTICATED, + UNAVAILABLE: grpcStatus.UNAVAILABLE, + UNKNOWN: grpcStatus.UNKNOWN, +})[code] ?? grpcStatus.UNKNOWN; + +const errorWithCode = (code, message) => { + const err = new GrpcError(grpcCodeFor(code), message); + err.legacyCode = code; + return err; +}; + +const upstreamError = (code, message, details = {}) => { + const err = errorWithCode(code, message); + err.httpStatus = Number.isFinite(Number(details.httpStatus)) ? Number(details.httpStatus) : 0; + err.httpBody = typeof details.httpBody === "string" ? details.httpBody : ""; + err.responseCode = Number.isFinite(Number(details.responseCode)) ? Number(details.responseCode) : undefined; + err.responseMessage = typeof details.responseMessage === "string" ? details.responseMessage : ""; + return err; +}; + +const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj ?? {}, key); + +const unwrapScalar = (value) => { + if (value === undefined || value === null) return undefined; + if (typeof value === "object" && value !== null && hasOwn(value, "value")) return unwrapScalar(value.value); + return value; +}; + +const firstDefined = (...values) => values.find((value) => value !== undefined && value !== null); + +const toTrimmedString = (value) => { + const raw = unwrapScalar(value); + if (raw === undefined || raw === null) return ""; + return String(raw).trim(); +}; + +const toBoolean = (value) => { + const raw = unwrapScalar(value); + if (typeof raw === "boolean") return raw; + if (typeof raw === "number") return Number.isFinite(raw) && raw !== 0; + if (typeof raw === "string") { + const text = raw.trim().toLowerCase(); + if (["1", "true", "yes", "y", "on"].includes(text)) return true; + if (["0", "false", "no", "n", "off", ""].includes(text)) return false; + } + return false; +}; + +const mergedBindings = (ctx = {}) => ({ + ...(ctx.config ?? {}), + ...(ctx.secret ?? {}), + ...(ctx.bindings ?? {}), +}); + +const resolveCallContext = (ctx = {}) => ({ + ...ctx, + bindings: mergedBindings(ctx), + limits: ctx.limits ?? {}, + meta: ctx.meta ?? {}, + req: ctx.req ?? ctx.request ?? {}, +}); + +const optionalUint32 = (value) => { + const raw = unwrapScalar(value); + if (raw === undefined || raw === null || raw === "") return undefined; + const num = Number(raw); + if (!Number.isFinite(num) || num <= 0) return undefined; + return Math.trunc(num); +}; + +const resolveTimeoutMs = (ctx = {}) => { + const bindings = ctx.bindings || mergedBindings(ctx); + return optionalUint32(ctx.limits?.timeoutMs) ?? optionalUint32(bindings.timeoutMs) ?? optionalUint32(bindings.timeout_ms) ?? DEFAULT_TIMEOUT_MS; +}; + +const optionalIntWithDefault = (value, fallback, minimum = 0) => { + const parsed = optionalUint32(value); + if (parsed === undefined || parsed < minimum) return fallback; + return parsed; +}; + +const resolveGatewayIntents = (bindings = {}) => optionalIntWithDefault(firstDefined(bindings.gatewayIntents, bindings.gateway_intents), DEFAULT_GATEWAY_INTENTS, 1); +const resolveGatewayShardIndex = (bindings = {}) => optionalIntWithDefault(firstDefined(bindings.gatewayShardIndex, bindings.gateway_shard_index), 0, 0); +const resolveGatewayShardCount = (bindings = {}) => optionalIntWithDefault(firstDefined(bindings.gatewayShardCount, bindings.gateway_shard_count), 1, 1); +const resolveMaxBufferedMessages = (bindings = {}) => optionalIntWithDefault(firstDefined(bindings.maxBufferedMessages, bindings.max_buffered_messages), DEFAULT_MAX_BUFFERED_MESSAGES, 1); +const resolveGatewayReconnectMs = (bindings = {}) => optionalIntWithDefault(firstDefined(bindings.gatewayReconnectMs, bindings.gateway_reconnect_ms), DEFAULT_GATEWAY_RECONNECT_MS, 100); +const resolveAutoStartGateway = (bindings = {}) => toBoolean(firstDefined(bindings.autoStartGateway, bindings.auto_start_gateway)); + +const requireString = (value, fieldName) => { + const text = toTrimmedString(value); + if (!text) throw errorWithCode("INVALID_ARGUMENT", `${fieldName} is required`); + return text; +}; + +const optionalInt32 = (value) => { + const raw = unwrapScalar(value); + if (raw === undefined || raw === null || raw === "") return undefined; + const num = Number(raw); + if (!Number.isFinite(num)) return undefined; + return Math.trunc(num); +}; + +const normalizeBaseUrl = (value, fallback, fieldName) => { + const raw = toTrimmedString(value) || fallback; + let parsed; + try { + parsed = new URL(raw); + } catch { + throw errorWithCode("INVALID_ARGUMENT", `${fieldName} must be a valid HTTP/HTTPS URL`); + } + if (!["http:", "https:"].includes(parsed.protocol)) { + throw errorWithCode("INVALID_ARGUMENT", `${fieldName} must be a valid HTTP/HTTPS URL`); + } + parsed.pathname = parsed.pathname.replace(/\/+$/, ""); + parsed.search = ""; + parsed.hash = ""; + return parsed.toString().replace(/\/$/, ""); +}; + +const resolveApiBaseUrl = (bindings = {}) => normalizeBaseUrl(firstDefined(bindings.baseUrl, bindings.base_url), DEFAULT_API_BASE_URL, "baseUrl"); +const resolveTokenUrl = (bindings = {}) => normalizeBaseUrl(firstDefined(bindings.tokenUrl, bindings.token_url), DEFAULT_TOKEN_URL, "tokenUrl"); +const resolveAppId = (bindings = {}) => toTrimmedString(firstDefined(bindings.appId, bindings.app_id)); +const resolveAppSecret = (bindings = {}) => toTrimmedString(firstDefined(bindings.appSecret, bindings.app_secret, bindings.clientSecret, bindings.client_secret)); +const resolveConfiguredAccessToken = (bindings = {}) => toTrimmedString(firstDefined(bindings.accessToken, bindings.access_token)); + +const parseJsonBody = (bodyText, label = "response") => { + const text = String(bodyText ?? ""); + if (!text.trim()) return {}; + try { + return JSON.parse(text); + } catch { + throw upstreamError("UNKNOWN", `qq bot ${label} body is not valid JSON`, { httpBody: text }); + } +}; + +const mapHttpStatusToCode = (status) => { + if (status === 401) return "UNAUTHENTICATED"; + if (status === 403) return "PERMISSION_DENIED"; + if (status === 429) return "UNAVAILABLE"; + if (status >= 400 && status < 500) return "FAILED_PRECONDITION"; + if (status >= 500) return "UNAVAILABLE"; + return "UNKNOWN"; +}; + +const mapErrorBodyToCode = (body = {}) => { + const code = Number(body.code ?? body.errcode); + if (code === 0 || !Number.isFinite(code)) return "UNKNOWN"; + if ([401, 11263].includes(code)) return "UNAUTHENTICATED"; + if ([403, 11264].includes(code)) return "PERMISSION_DENIED"; + if (code === 22009 || code === 429) return "UNAVAILABLE"; + return "FAILED_PRECONDITION"; +}; + +const tokenCacheKey = (tokenUrl, appId, appSecret) => `${tokenUrl}\n${appId}\n${appSecret}`; + +const readTokenCache = (key, nowMs = Date.now()) => { + const cached = tokenCache.get(key); + if (!cached || !cached.accessToken) return ""; + if (cached.expiresAtMs <= nowMs + 60_000) return ""; + return cached.accessToken; +}; + +const writeTokenCache = (key, accessToken, expiresIn, nowMs = Date.now()) => { + const ttlMs = Math.max(0, Number(expiresIn || 0) * 1000); + tokenCache.set(key, { + accessToken, + expiresAtMs: nowMs + ttlMs, + expiresIn: Number(expiresIn || 0), + }); +}; + +const fetchAppAccessToken = async (ctx = {}, forceRefresh = false) => { + const callCtx = resolveCallContext(ctx); + const bindings = callCtx.bindings || {}; + const configured = resolveConfiguredAccessToken(bindings); + if (configured && !forceRefresh) { + return { + success: true, + http_status: 0, + access_token: configured, + expires_in: 0, + http_body: "", + }; + } + + const appId = requireString(resolveAppId(bindings), "appId"); + const appSecret = requireString(resolveAppSecret(bindings), "appSecret"); + const tokenUrl = resolveTokenUrl(bindings); + const cacheKey = tokenCacheKey(tokenUrl, appId, appSecret); + const cachedToken = forceRefresh ? "" : readTokenCache(cacheKey); + if (cachedToken) { + return { + success: true, + http_status: 0, + access_token: cachedToken, + expires_in: 0, + http_body: "", + }; + } + + let response; + try { + response = await fetch(tokenUrl, { + method: "POST", + timeoutMs: resolveTimeoutMs(callCtx), + headers: { + "content-type": "application/json", + accept: "application/json, */*;q=0.8", + }, + body: JSON.stringify({ appId, clientSecret: appSecret }), + }); + } catch (err) { + throw upstreamError("UNAVAILABLE", "qq bot access token request failed", { + httpStatus: 0, + responseMessage: err?.cause?.message || err?.message || "fetch failed", + }); + } + + const httpStatus = Number(response.status || 0); + const httpBody = String((await response.text()) ?? ""); + const body = parseJsonBody(httpBody, "access token response"); + if (httpStatus < 200 || httpStatus >= 300) { + throw upstreamError(mapHttpStatusToCode(httpStatus), `qq bot token http ${httpStatus}`, { + httpStatus, + httpBody, + responseCode: body.code ?? body.errcode, + responseMessage: body.message ?? body.errmsg, + }); + } + + const accessToken = requireString(body.access_token, "access_token"); + const expiresIn = optionalInt32(body.expires_in) ?? 0; + writeTokenCache(cacheKey, accessToken, expiresIn); + return { + success: true, + http_status: httpStatus, + access_token: accessToken, + expires_in: expiresIn, + http_body: httpBody, + }; +}; + +const resolveAccessToken = async (ctx = {}) => { + const callCtx = resolveCallContext(ctx); + const configured = resolveConfiguredAccessToken(callCtx.bindings || {}); + if (configured) return configured; + return (await fetchAppAccessToken(callCtx, false)).access_token; +}; + +const gatewayStatus = () => ({ + running: gatewayState.running, + ready: gatewayState.ready, + state: gatewayState.state, + session_id: gatewayState.sessionId, + gateway_url: gatewayState.gatewayUrl, + last_event_type: gatewayState.lastEventType, + last_error: gatewayState.lastError, + started_at: gatewayState.startedAt, + last_event_at: gatewayState.lastEventAt, + last_heartbeat_ack_at: gatewayState.lastHeartbeatAckAt, + queue_size: gatewayState.queue.length, + dropped_messages: gatewayState.droppedMessages, + reconnect_count: gatewayState.reconnectCount, + intents: gatewayState.intents, +}); + +const clearGatewayTimers = () => { + if (gatewayState.heartbeatTimer) clearInterval(gatewayState.heartbeatTimer); + if (gatewayState.reconnectTimer) clearTimeout(gatewayState.reconnectTimer); + gatewayState.heartbeatTimer = null; + gatewayState.reconnectTimer = null; +}; + +const closeGatewaySocket = () => { + const ws = gatewayState.ws; + gatewayState.ws = null; + if (!ws) return; + try { + ws.close(); + } catch { + // Ignore best-effort close errors. + } +}; + +const gatewayReceiveTargets = new Set([ + "C2C_MESSAGE_CREATE", + "GROUP_AT_MESSAGE_CREATE", + "GROUP_MESSAGE_CREATE", + "DIRECT_MESSAGE_CREATE", + "AT_MESSAGE_CREATE", + "MESSAGE_CREATE", +]); + +const enqueueGatewayMessage = async (payload) => { + const event = await handleNormalizeEvent({ payload_json: JSON.stringify(payload) }); + if (!event.is_message) return; + const maxBuffered = resolveMaxBufferedMessages(gatewayState.ctx?.bindings || {}); + gatewayState.localMessageSeq += 1; + gatewayState.queue.push({ + local_id: String(gatewayState.localMessageSeq), + received_at: new Date().toISOString(), + event, + }); + while (gatewayState.queue.length > maxBuffered) { + gatewayState.queue.shift(); + gatewayState.droppedMessages += 1; + } +}; + +const fetchGatewayInfo = async (ctx = {}, accessToken) => { + const baseUrl = resolveApiBaseUrl(resolveCallContext(ctx).bindings || {}); + const response = await fetch(buildApiUrl(baseUrl, "gateway/bot"), { + method: "GET", + timeoutMs: resolveTimeoutMs(ctx), + headers: { + authorization: `QQBot ${accessToken}`, + accept: "application/json, */*;q=0.8", + }, + }); + const httpStatus = Number(response.status || 0); + const httpBody = String((await response.text()) ?? ""); + const body = parseJsonBody(httpBody, "gateway response"); + if (httpStatus < 200 || httpStatus >= 300 || !body.url) { + throw upstreamError(mapHttpStatusToCode(httpStatus), `qq bot gateway http ${httpStatus}`, { + httpStatus, + httpBody, + responseCode: body.code ?? body.errcode, + responseMessage: body.message ?? body.errmsg, + }); + } + return body; +}; + +const scheduleGatewayReconnect = () => { + if (!gatewayState.running || gatewayState.reconnectTimer) return; + const delayMs = resolveGatewayReconnectMs(gatewayState.ctx?.bindings || {}); + gatewayState.state = "reconnecting"; + gatewayState.reconnectTimer = setTimeout(() => { + gatewayState.reconnectTimer = null; + gatewayState.reconnectCount += 1; + connectGateway().catch((err) => { + gatewayState.lastError = err?.message || String(err); + scheduleGatewayReconnect(); + }); + }, delayMs); +}; + +const identifyGateway = (accessToken) => { + const bindings = gatewayState.ctx?.bindings || {}; + const shardIndex = resolveGatewayShardIndex(bindings); + const shardCount = resolveGatewayShardCount(bindings); + gatewayState.ws?.send(JSON.stringify({ + op: 2, + d: { + token: `QQBot ${accessToken}`, + intents: gatewayState.intents, + shard: [shardIndex, shardCount], + properties: {}, + }, + })); +}; + +const sendHeartbeat = () => { + if (!gatewayState.ws || gatewayState.ws.readyState !== WebSocket.OPEN) return; + gatewayState.ws.send(JSON.stringify({ op: 1, d: gatewayState.seq })); +}; + +const handleGatewayPayload = async (payload, accessToken) => { + if (payload.s !== undefined && payload.s !== null) gatewayState.seq = payload.s; + if (payload.t) gatewayState.lastEventType = String(payload.t); + + if (payload.op === 10) { + const interval = Number(payload.d?.heartbeat_interval || 45_000); + if (gatewayState.heartbeatTimer) clearInterval(gatewayState.heartbeatTimer); + gatewayState.heartbeatTimer = setInterval(sendHeartbeat, interval); + identifyGateway(accessToken); + return; + } + if (payload.op === 11) { + gatewayState.lastHeartbeatAckAt = new Date().toISOString(); + return; + } + if (payload.op === 7 || payload.op === 9) { + gatewayState.lastError = payload.op === 7 ? "gateway requested reconnect" : "gateway invalid session"; + closeGatewaySocket(); + scheduleGatewayReconnect(); + return; + } + if (payload.op !== 0) return; + + gatewayState.lastEventAt = new Date().toISOString(); + if (payload.t === "READY") { + gatewayState.ready = true; + gatewayState.state = "ready"; + gatewayState.sessionId = toTrimmedString(payload.d?.session_id); + return; + } + if (gatewayReceiveTargets.has(String(payload.t))) { + await enqueueGatewayMessage(payload); + } +}; + +const connectGateway = async () => { + if (!gatewayState.running || !gatewayState.ctx) return; + gatewayState.state = "connecting"; + gatewayState.ready = false; + gatewayState.lastError = ""; + clearGatewayTimers(); + closeGatewaySocket(); + + const accessToken = await resolveAccessToken(gatewayState.ctx); + const gatewayInfo = await fetchGatewayInfo(gatewayState.ctx, accessToken); + gatewayState.gatewayUrl = toTrimmedString(gatewayInfo.url); + + const WebSocketCtor = globalThis.WebSocket; + if (typeof WebSocketCtor !== "function") { + throw errorWithCode("FAILED_PRECONDITION", "global WebSocket is not available in this Node.js runtime"); + } + + const ws = new WebSocketCtor(gatewayState.gatewayUrl); + gatewayState.ws = ws; + + ws.addEventListener("open", () => { + gatewayState.state = "open"; + }); + ws.addEventListener("message", (event) => { + Promise.resolve() + .then(async () => { + const payload = JSON.parse(String(event.data)); + await handleGatewayPayload(payload, accessToken); + }) + .catch((err) => { + gatewayState.lastError = err?.message || String(err); + }); + }); + ws.addEventListener("error", (event) => { + gatewayState.lastError = event?.message || "gateway websocket error"; + }); + ws.addEventListener("close", () => { + gatewayState.ready = false; + clearGatewayTimers(); + if (gatewayState.running) scheduleGatewayReconnect(); + else gatewayState.state = "stopped"; + }); +}; + +const startGateway = async (ctx = {}, forceRestart = false) => { + const callCtx = resolveCallContext(ctx); + if (gatewayState.running && !forceRestart) return gatewayStatus(); + await stopGateway(); + gatewayState.running = true; + gatewayState.ready = false; + gatewayState.state = "starting"; + gatewayState.startedAt = new Date().toISOString(); + gatewayState.ctx = callCtx; + gatewayState.intents = resolveGatewayIntents(callCtx.bindings || {}); + gatewayState.seq = null; + await connectGateway(); + return gatewayStatus(); +}; + +const stopGateway = async () => { + gatewayState.running = false; + gatewayState.ready = false; + gatewayState.state = "stopped"; + clearGatewayTimers(); + closeGatewaySocket(); + return gatewayStatus(); +}; + +const pollMessages = (req = {}) => { + const maxMessages = optionalIntWithDefault(firstDefined(req.max_messages, req.maxMessages), 10, 1); + const messages = gatewayState.queue.slice(0, maxMessages); + if (toBoolean(req.ack)) { + gatewayState.queue.splice(0, messages.length); + } + return { + messages, + queue_size: gatewayState.queue.length, + }; +}; + +const ackMessage = (req = {}) => { + if (toBoolean(req.all)) { + const acked = gatewayState.queue.length; + gatewayState.queue = []; + return { acked, queue_size: 0 }; + } + const rawIds = firstDefined(req.local_id, req.localId); + const ids = Array.isArray(rawIds) ? rawIds.map(toTrimmedString).filter(Boolean) : []; + const idSet = new Set(ids); + const before = gatewayState.queue.length; + gatewayState.queue = gatewayState.queue.filter((message) => !idSet.has(message.local_id)); + return { + acked: before - gatewayState.queue.length, + queue_size: gatewayState.queue.length, + }; +}; + +const buildJsonObject = (jsonText, fieldName) => { + const text = toTrimmedString(jsonText); + if (!text) return undefined; + try { + const parsed = JSON.parse(text); + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new Error(`${fieldName} must be a JSON object`); + } + return parsed; + } catch (err) { + throw errorWithCode("INVALID_ARGUMENT", err?.message || `${fieldName} must be valid JSON`); + } +}; + +const addJsonObject = (payload, fieldName, jsonText) => { + const parsed = buildJsonObject(jsonText, `${fieldName}_json`); + if (parsed !== undefined) payload[fieldName] = parsed; +}; + +const buildMessagePayload = (req = {}, options = {}) => { + const msgType = optionalInt32(firstDefined(req.msg_type, req.msgType)) ?? 0; + const payload = { msg_type: msgType }; + const content = toTrimmedString(req.content); + if (content) payload.content = content; + if (msgType === 0 && !content) throw errorWithCode("INVALID_ARGUMENT", "content is required for text messages"); + + addJsonObject(payload, "markdown", firstDefined(req.markdown_json, req.markdownJson)); + addJsonObject(payload, "keyboard", firstDefined(req.keyboard_json, req.keyboardJson)); + addJsonObject(payload, "ark", firstDefined(req.ark_json, req.arkJson)); + addJsonObject(payload, "media", firstDefined(req.media_json, req.mediaJson)); + addJsonObject(payload, "message_reference", firstDefined(req.message_reference_json, req.messageReferenceJson)); + + const eventId = toTrimmedString(firstDefined(req.event_id, req.eventId)); + const msgId = toTrimmedString(firstDefined(req.msg_id, req.msgId)); + const msgSeq = optionalInt32(firstDefined(req.msg_seq, req.msgSeq)); + if (eventId) payload.event_id = eventId; + if (msgId) payload.msg_id = msgId; + if (msgSeq !== undefined) payload.msg_seq = msgSeq; + if (options.allowWakeup && toBoolean(firstDefined(req.is_wakeup, req.isWakeup))) payload.is_wakeup = true; + return payload; +}; + +const buildApiUrl = (baseUrl, path) => new URL(path, `${baseUrl}/`).toString(); + +const callOpenApi = async (ctx = {}, path, payload = {}) => { + const callCtx = resolveCallContext(ctx); + const baseUrl = resolveApiBaseUrl(callCtx.bindings || {}); + const accessToken = await resolveAccessToken(callCtx); + const url = buildApiUrl(baseUrl, path.replace(/^\/+/, "")); + + let response; + try { + response = await fetch(url, { + method: "POST", + timeoutMs: resolveTimeoutMs(callCtx), + headers: { + authorization: `QQBot ${accessToken}`, + "content-type": "application/json", + accept: "application/json, */*;q=0.8", + }, + body: JSON.stringify(payload), + }); + } catch (err) { + throw upstreamError("UNAVAILABLE", "qq bot openapi request failed", { + httpStatus: 0, + responseMessage: err?.cause?.message || err?.message || "fetch failed", + }); + } + + const httpStatus = Number(response.status || 0); + const httpBody = String((await response.text()) ?? ""); + const body = parseJsonBody(httpBody, "openapi response"); + if (httpStatus < 200 || httpStatus >= 300) { + throw upstreamError(mapHttpStatusToCode(httpStatus), `qq bot openapi http ${httpStatus}`, { + httpStatus, + httpBody, + responseCode: body.code ?? body.errcode, + responseMessage: body.message ?? body.errmsg, + }); + } + if (body.code !== undefined && Number(body.code) !== 0) { + throw upstreamError(mapErrorBodyToCode(body), "qq bot openapi business failure", { + httpStatus, + httpBody, + responseCode: body.code, + responseMessage: body.message, + }); + } + return { httpStatus, httpBody, body }; +}; + +const buildSendMessageResponse = ({ httpStatus, httpBody, body }) => ({ + success: true, + http_status: httpStatus, + id: toTrimmedString(body.id), + timestamp: toTrimmedString(body.timestamp), + http_body: httpBody, +}); + +const handleGetAccessToken = async (req = {}, ctx = {}) => fetchAppAccessToken(ctx, toBoolean(firstDefined(req.force_refresh, req.forceRefresh))); + +const handleStartGateway = async (req = {}, ctx = {}) => startGateway(ctx, toBoolean(firstDefined(req.force_restart, req.forceRestart))); +const handleStopGateway = async () => stopGateway(); +const handleGetGatewayStatus = async () => gatewayStatus(); +const handlePollMessages = async (req = {}) => pollMessages(req); +const handleAckMessage = async (req = {}) => ackMessage(req); + +const handleSendC2CMessage = async (req = {}, ctx = {}) => { + const openid = requireString(firstDefined(req.openid, req.openId), "openid"); + const payload = buildMessagePayload(req, { allowWakeup: true }); + return buildSendMessageResponse(await callOpenApi(ctx, `/v2/users/${encodeURIComponent(openid)}/messages`, payload)); +}; + +const handleSendGroupMessage = async (req = {}, ctx = {}) => { + const groupOpenid = requireString(firstDefined(req.group_openid, req.groupOpenid), "group_openid"); + const payload = buildMessagePayload(req, { allowWakeup: false }); + return buildSendMessageResponse(await callOpenApi(ctx, `/v2/groups/${encodeURIComponent(groupOpenid)}/messages`, payload)); +}; + +const parsePayload = (req = {}) => { + const candidate = firstDefined(req.payload_json, req.payloadJson, req.payload); + if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) return candidate; + const text = toTrimmedString(candidate); + if (!text) throw errorWithCode("INVALID_ARGUMENT", "payload_json is required"); + try { + const parsed = JSON.parse(text); + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new Error("payload_json must be a JSON object"); + } + return parsed; + } catch (err) { + throw errorWithCode("INVALID_ARGUMENT", err?.message || "payload_json must be valid JSON"); + } +}; + +const stringifyId = (value) => { + const raw = unwrapScalar(value); + if (raw === undefined || raw === null || raw === "") return ""; + if (typeof raw === "number" && Number.isFinite(raw)) return String(Math.trunc(raw)); + return String(raw); +}; + +const eventData = (payload = {}) => { + if (payload.d && typeof payload.d === "object" && !Array.isArray(payload.d)) return payload.d; + return payload; +}; + +const handleNormalizeEvent = async (req = {}) => { + const payload = parsePayload(req); + const data = eventData(payload); + const author = data.author && typeof data.author === "object" ? data.author : {}; + const eventType = stringifyId(payload.t ?? data.t); + const openid = stringifyId(author.user_openid ?? author.member_openid ?? author.id ?? data.openid); + const groupOpenid = stringifyId(data.group_openid); + const channelId = stringifyId(data.channel_id); + const guildId = stringifyId(data.guild_id); + const messageTypes = new Set(["C2C_MESSAGE_CREATE", "GROUP_AT_MESSAGE_CREATE", "GROUP_MESSAGE_CREATE", "DIRECT_MESSAGE_CREATE", "AT_MESSAGE_CREATE", "MESSAGE_CREATE"]); + + return { + is_message: messageTypes.has(eventType) || Boolean(data.content && (openid || groupOpenid || channelId)), + is_c2c: eventType === "C2C_MESSAGE_CREATE" || Boolean(openid && !groupOpenid && !channelId), + is_group: eventType === "GROUP_AT_MESSAGE_CREATE" || eventType === "GROUP_MESSAGE_CREATE" || Boolean(groupOpenid), + is_channel: eventType === "DIRECT_MESSAGE_CREATE" || eventType === "AT_MESSAGE_CREATE" || eventType === "MESSAGE_CREATE" || Boolean(channelId || guildId), + event_id: stringifyId(payload.id), + op: optionalInt32(payload.op) ?? 0, + event_type: eventType, + seq: stringifyId(payload.s ?? data.seq), + message_id: stringifyId(data.id), + content: stringifyId(data.content), + timestamp: stringifyId(data.timestamp), + openid, + group_openid: groupOpenid, + channel_id: channelId, + guild_id: guildId, + author_json: JSON.stringify(author), + attachments_json: JSON.stringify(Array.isArray(data.attachments) ? data.attachments : []), + data_json: JSON.stringify(data), + payload_json: JSON.stringify(payload), + }; +}; + +const registerHandlers = (ctx = {}) => { + const callCtx = resolveCallContext(ctx); + return { + [METHOD_GET_ACCESS_TOKEN_PATH]: (req = callCtx.req) => handleGetAccessToken(req ?? {}, callCtx), + [METHOD_START_GATEWAY_PATH]: (req = callCtx.req) => handleStartGateway(req ?? {}, callCtx), + [METHOD_STOP_GATEWAY_PATH]: () => handleStopGateway(), + [METHOD_GET_GATEWAY_STATUS_PATH]: () => handleGetGatewayStatus(), + [METHOD_POLL_MESSAGES_PATH]: (req = callCtx.req) => handlePollMessages(req ?? {}), + [METHOD_ACK_MESSAGE_PATH]: (req = callCtx.req) => handleAckMessage(req ?? {}), + [METHOD_SEND_C2C_MESSAGE_PATH]: (req = callCtx.req) => handleSendC2CMessage(req ?? {}, callCtx), + [METHOD_SEND_GROUP_MESSAGE_PATH]: (req = callCtx.req) => handleSendGroupMessage(req ?? {}, callCtx), + [METHOD_NORMALIZE_EVENT_PATH]: (req = callCtx.req) => handleNormalizeEvent(req ?? {}, callCtx), + }; +}; + +export function rpcdef(ctx = {}) { + return registerHandlers(ctx); +} + +const callSdkHandler = (ctx, path) => registerHandlers(ctx)[path](ctx?.request ?? ctx?.req ?? {}); + +export const handlers = { + [METHOD_GET_ACCESS_TOKEN_FULL]: (ctx) => callSdkHandler(ctx, METHOD_GET_ACCESS_TOKEN_PATH), + [METHOD_START_GATEWAY_FULL]: (ctx) => callSdkHandler(ctx, METHOD_START_GATEWAY_PATH), + [METHOD_STOP_GATEWAY_FULL]: (ctx) => callSdkHandler(ctx, METHOD_STOP_GATEWAY_PATH), + [METHOD_GET_GATEWAY_STATUS_FULL]: (ctx) => callSdkHandler(ctx, METHOD_GET_GATEWAY_STATUS_PATH), + [METHOD_POLL_MESSAGES_FULL]: (ctx) => callSdkHandler(ctx, METHOD_POLL_MESSAGES_PATH), + [METHOD_ACK_MESSAGE_FULL]: (ctx) => callSdkHandler(ctx, METHOD_ACK_MESSAGE_PATH), + [METHOD_SEND_C2C_MESSAGE_FULL]: (ctx) => callSdkHandler(ctx, METHOD_SEND_C2C_MESSAGE_PATH), + [METHOD_SEND_GROUP_MESSAGE_FULL]: (ctx) => callSdkHandler(ctx, METHOD_SEND_GROUP_MESSAGE_PATH), + [METHOD_NORMALIZE_EVENT_FULL]: (ctx) => callSdkHandler(ctx, METHOD_NORMALIZE_EVENT_PATH), +}; + +const parseRuntimeJsonArg = (argv, name) => { + const index = argv.indexOf(name); + if (index >= 0 && argv[index + 1]) return JSON.parse(argv[index + 1]); + const prefix = `${name}=`; + const inline = argv.find((arg) => arg.startsWith(prefix)); + if (inline) return JSON.parse(inline.slice(prefix.length)); + return undefined; +}; + +const parseRuntimeFileArg = (argv, name) => { + const index = argv.indexOf(name); + if (index >= 0 && argv[index + 1]) return JSON.parse(fs.readFileSync(argv[index + 1], "utf8")); + const prefix = `${name}=`; + const inline = argv.find((arg) => arg.startsWith(prefix)); + if (inline) return JSON.parse(fs.readFileSync(inline.slice(prefix.length), "utf8")); + return undefined; +}; + +const readRuntimeObject = (argv, fileArg, jsonArg) => parseRuntimeJsonArg(argv, jsonArg) ?? parseRuntimeFileArg(argv, fileArg) ?? {}; + +export async function maybeAutoStartGatewayFromCli(argv = process.argv.slice(2)) { + if (argv[0] !== "--runtime" || !["serve", "dev"].includes(argv[1])) return false; + const config = readRuntimeObject(argv, "--config", "--config-json"); + if (!resolveAutoStartGateway(config)) return false; + const secret = readRuntimeObject(argv, "--secret", "--secret-json"); + await startGateway({ config, secret }, true); + return true; +} + +export const _test = { + buildApiUrl, + buildJsonObject, + buildMessagePayload, + buildSendMessageResponse, + callOpenApi, + clearGatewayTimers, + closeGatewaySocket, + connectGateway, + errorWithCode, + enqueueGatewayMessage, + eventData, + fetchGatewayInfo, + fetchAppAccessToken, + firstDefined, + gatewayState, + gatewayStatus, + grpcCodeFor, + handleAckMessage, + handleGetAccessToken, + handleGetGatewayStatus, + handleNormalizeEvent, + handlePollMessages, + handleSendC2CMessage, + handleSendGroupMessage, + handleStartGateway, + handleStopGateway, + hasOwn, + maybeAutoStartGatewayFromCli, + mapErrorBodyToCode, + mapHttpStatusToCode, + mergedBindings, + normalizeBaseUrl, + optionalInt32, + optionalIntWithDefault, + optionalUint32, + parseJsonBody, + parsePayload, + pollMessages, + readTokenCache, + registerHandlers, + requireString, + resolveAccessToken, + resolveApiBaseUrl, + resolveAppId, + resolveAppSecret, + resolveCallContext, + resolveConfiguredAccessToken, + resolveAutoStartGateway, + resolveGatewayIntents, + resolveGatewayReconnectMs, + resolveGatewayShardCount, + resolveGatewayShardIndex, + resolveMaxBufferedMessages, + resolveTimeoutMs, + resolveTokenUrl, + startGateway, + stopGateway, + stringifyId, + toBoolean, + tokenCache, + tokenCacheKey, + toTrimmedString, + unwrapScalar, + upstreamError, + writeTokenCache, +}; diff --git a/services/tencent__qq-chat/test/tencent-qq-chat.test.js b/services/tencent__qq-chat/test/tencent-qq-chat.test.js new file mode 100644 index 00000000..e64ec99c --- /dev/null +++ b/services/tencent__qq-chat/test/tencent-qq-chat.test.js @@ -0,0 +1,408 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { GrpcError, grpcStatus } from "@chaitin-ai/octobus-sdk"; + +import { + METHOD_ACK_MESSAGE_PATH, + METHOD_GET_GATEWAY_STATUS_PATH, + METHOD_GET_ACCESS_TOKEN_PATH, + METHOD_NORMALIZE_EVENT_FULL, + METHOD_NORMALIZE_EVENT_PATH, + METHOD_POLL_MESSAGES_PATH, + METHOD_SEND_C2C_MESSAGE_FULL, + METHOD_SEND_C2C_MESSAGE_PATH, + METHOD_SEND_GROUP_MESSAGE_PATH, + METHOD_START_GATEWAY_PATH, + METHOD_STOP_GATEWAY_PATH, + _test, + handlers, + rpcdef, +} from "../src/tencent-qq-chat.js"; +import { service } from "../src/service.js"; + +const originalFetch = globalThis.fetch; +const originalWebSocket = globalThis.WebSocket; + +const response = (status, body) => ({ + status, + text: async () => body, +}); + +const setFetch = (impl) => { + globalThis.fetch = impl; +}; + +const buildCtx = (overrides = {}) => ({ + config: { + baseUrl: "https://api.sgroup.qq.com", + tokenUrl: "https://bots.qq.com/app/getAppAccessToken", + ...(overrides.config || {}), + }, + secret: { + appId: "app-1", + appSecret: "secret-1", + ...(overrides.secret || {}), + }, + bindings: overrides.bindings || {}, + limits: { timeoutMs: 1000, ...(overrides.limits || {}) }, + req: overrides.req || {}, +}); + +const expectGrpcError = async (fn, legacyCode, code) => { + let caught; + try { + await fn(); + } catch (err) { + caught = err; + } + assert.ok(caught, "expected function to reject"); + assert.ok(caught instanceof GrpcError); + assert.equal(caught.legacyCode, legacyCode); + assert.equal(caught.code, code); + return caught; +}; + +test.afterEach(async () => { + globalThis.fetch = originalFetch; + globalThis.WebSocket = originalWebSocket; + _test.tokenCache.clear(); + await _test.stopGateway(); + _test.gatewayState.queue = []; + _test.gatewayState.droppedMessages = 0; + _test.gatewayState.localMessageSeq = 0; +}); + +test("service exports defineService result and handlers", () => { + assert.equal(typeof service, "object"); + assert.equal(typeof handlers[METHOD_SEND_C2C_MESSAGE_FULL], "function"); + assert.equal(typeof handlers[METHOD_NORMALIZE_EVENT_FULL], "function"); +}); + +test("GetAccessToken posts AppID and AppSecret to official token endpoint", async () => { + let captured; + setFetch(async (url, init) => { + captured = { url: String(url), init }; + return response(200, JSON.stringify({ + access_token: "access-1", + expires_in: "7200", + })); + }); + + const result = await rpcdef(buildCtx())[METHOD_GET_ACCESS_TOKEN_PATH]({}); + + assert.equal(captured.url, "https://bots.qq.com/app/getAppAccessToken"); + assert.equal(captured.init.method, "POST"); + assert.deepEqual(JSON.parse(captured.init.body), { + appId: "app-1", + clientSecret: "secret-1", + }); + assert.equal(result.success, true); + assert.equal(result.access_token, "access-1"); + assert.equal(result.expires_in, 7200); +}); + +test("SendC2CMessage fetches token and posts to /v2/users/{openid}/messages", async () => { + const calls = []; + setFetch(async (url, init) => { + calls.push({ url: String(url), init }); + if (calls.length === 1) { + return response(200, JSON.stringify({ + access_token: "access-1", + expires_in: 7200, + })); + } + return response(200, JSON.stringify({ + id: "msg-1", + timestamp: 1710000000, + })); + }); + + const result = await rpcdef(buildCtx())[METHOD_SEND_C2C_MESSAGE_PATH]({ + openid: "USER_OPENID", + content: "hello", + msg_id: "incoming-1", + msg_seq: 2, + }); + + assert.equal(calls.length, 2); + assert.equal(calls[1].url, "https://api.sgroup.qq.com/v2/users/USER_OPENID/messages"); + assert.equal(calls[1].init.headers.authorization, "QQBot access-1"); + assert.deepEqual(JSON.parse(calls[1].init.body), { + msg_type: 0, + content: "hello", + msg_id: "incoming-1", + msg_seq: 2, + }); + assert.equal(result.success, true); + assert.equal(result.id, "msg-1"); + assert.equal(result.timestamp, "1710000000"); +}); + +test("SendGroupMessage uses configured access token and rich message JSON fields", async () => { + let captured; + setFetch(async (url, init) => { + captured = { url: String(url), init }; + return response(200, JSON.stringify({ + id: "group-msg-1", + timestamp: "2026-06-29T12:00:00+08:00", + })); + }); + + const result = await rpcdef(buildCtx({ + secret: { + accessToken: "configured-token", + }, + }))[METHOD_SEND_GROUP_MESSAGE_PATH]({ + groupOpenid: "GROUP_OPENID", + content: "# title", + msgType: 2, + markdownJson: '{"content":"# title"}', + keyboardJson: '{"id":"keyboard-template"}', + eventId: "event-1", + }); + + assert.equal(captured.url, "https://api.sgroup.qq.com/v2/groups/GROUP_OPENID/messages"); + assert.equal(captured.init.headers.authorization, "QQBot configured-token"); + assert.deepEqual(JSON.parse(captured.init.body), { + msg_type: 2, + content: "# title", + markdown: { content: "# title" }, + keyboard: { id: "keyboard-template" }, + event_id: "event-1", + }); + assert.equal(result.id, "group-msg-1"); +}); + +test("NormalizeEvent maps official C2C and group payloads", async () => { + const c2cPayload = { + id: "event-1", + op: 0, + s: 42, + t: "C2C_MESSAGE_CREATE", + d: { + id: "ROBOT1.0_msg", + content: "hello", + timestamp: "2023-11-06T13:37:18+08:00", + author: { + user_openid: "USER_OPENID", + }, + }, + }; + + const c2c = await rpcdef(buildCtx())[METHOD_NORMALIZE_EVENT_PATH]({ + payload_json: JSON.stringify(c2cPayload), + }); + + assert.equal(c2c.is_message, true); + assert.equal(c2c.is_c2c, true); + assert.equal(c2c.is_group, false); + assert.equal(c2c.event_id, "event-1"); + assert.equal(c2c.event_type, "C2C_MESSAGE_CREATE"); + assert.equal(c2c.message_id, "ROBOT1.0_msg"); + assert.equal(c2c.openid, "USER_OPENID"); + + const group = await _test.handleNormalizeEvent({ + payload: { + t: "GROUP_AT_MESSAGE_CREATE", + d: { + id: "group-msg", + group_openid: "GROUP_OPENID", + content: " at bot", + author: { + member_openid: "MEMBER_OPENID", + member_role: "owner", + }, + }, + }, + }); + + assert.equal(group.is_group, true); + assert.equal(group.group_openid, "GROUP_OPENID"); + assert.equal(group.openid, "MEMBER_OPENID"); + assert.ok(group.author_json.includes("member_role")); +}); + +test("Gateway receiver starts, buffers messages, polls, and acks", async () => { + const fetchCalls = []; + setFetch(async (url, init) => { + fetchCalls.push({ url: String(url), init }); + return response(200, JSON.stringify({ + url: "wss://api.sgroup.qq.com/websocket", + shards: 1, + })); + }); + + class FakeWebSocket { + static OPEN = 1; + static instances = []; + + constructor(url) { + this.url = url; + this.readyState = FakeWebSocket.OPEN; + this.listeners = new Map(); + this.sent = []; + FakeWebSocket.instances.push(this); + queueMicrotask(() => this.emit("open", {})); + } + + addEventListener(name, callback) { + const listeners = this.listeners.get(name) || []; + listeners.push(callback); + this.listeners.set(name, listeners); + } + + send(data) { + this.sent.push(JSON.parse(data)); + } + + close() { + this.readyState = 3; + this.emit("close", {}); + } + + emit(name, event) { + for (const callback of this.listeners.get(name) || []) callback(event); + } + } + + globalThis.WebSocket = FakeWebSocket; + + const ctx = buildCtx({ + secret: { accessToken: "gateway-token" }, + config: { + baseUrl: "https://api.sgroup.qq.com", + gatewayIntents: 1 << 25, + maxBufferedMessages: 5, + }, + }); + + const start = await rpcdef(ctx)[METHOD_START_GATEWAY_PATH]({}); + assert.equal(start.running, true); + assert.equal(fetchCalls[0].url, "https://api.sgroup.qq.com/gateway/bot"); + + const socket = FakeWebSocket.instances[0]; + socket.emit("message", { + data: JSON.stringify({ + op: 10, + d: { heartbeat_interval: 100000 }, + }), + }); + await new Promise((resolve) => setTimeout(resolve, 0)); + assert.deepEqual(socket.sent[0], { + op: 2, + d: { + token: "QQBot gateway-token", + intents: 1 << 25, + shard: [0, 1], + properties: {}, + }, + }); + + socket.emit("message", { + data: JSON.stringify({ + op: 0, + s: 1, + t: "READY", + d: { session_id: "session-1" }, + }), + }); + await new Promise((resolve) => setTimeout(resolve, 0)); + + socket.emit("message", { + data: JSON.stringify({ + id: "event-1", + op: 0, + s: 2, + t: "C2C_MESSAGE_CREATE", + d: { + id: "msg-1", + content: "hello", + author: { user_openid: "USER_OPENID" }, + }, + }), + }); + await new Promise((resolve) => setTimeout(resolve, 0)); + + const status = await rpcdef(ctx)[METHOD_GET_GATEWAY_STATUS_PATH]({}); + assert.equal(status.ready, true); + assert.equal(status.session_id, "session-1"); + assert.equal(status.queue_size, 1); + + const polled = await rpcdef(ctx)[METHOD_POLL_MESSAGES_PATH]({ max_messages: 10 }); + assert.equal(polled.messages.length, 1); + assert.equal(polled.messages[0].event.event_type, "C2C_MESSAGE_CREATE"); + assert.equal(polled.messages[0].event.openid, "USER_OPENID"); + + const acked = await rpcdef(ctx)[METHOD_ACK_MESSAGE_PATH]({ localId: [polled.messages[0].local_id] }); + assert.equal(acked.acked, 1); + assert.equal(acked.queue_size, 0); + + const stopped = await rpcdef(ctx)[METHOD_STOP_GATEWAY_PATH]({}); + assert.equal(stopped.running, false); +}); + +test("SDK handlers read ctx.request", async () => { + setFetch(async () => response(200, JSON.stringify({ + id: "msg-sdk", + timestamp: 123, + }))); + + const result = await handlers[METHOD_SEND_C2C_MESSAGE_FULL]({ + request: { openid: "USER_OPENID", content: "hello" }, + config: { baseUrl: "https://api.sgroup.qq.com" }, + secret: { accessToken: "token-sdk" }, + }); + + assert.equal(result.id, "msg-sdk"); +}); + +test("validation and upstream failures map to gRPC errors", async () => { + await expectGrpcError( + () => rpcdef({ config: {}, secret: {} })[METHOD_SEND_C2C_MESSAGE_PATH]({ openid: "1", content: "x" }), + "INVALID_ARGUMENT", + grpcStatus.INVALID_ARGUMENT, + ); + + await expectGrpcError( + () => rpcdef(buildCtx({ secret: { accessToken: "token" } }))[METHOD_SEND_C2C_MESSAGE_PATH]({ openid: "", content: "x" }), + "INVALID_ARGUMENT", + grpcStatus.INVALID_ARGUMENT, + ); + + await expectGrpcError( + () => rpcdef(buildCtx({ secret: { accessToken: "token" } }))[METHOD_SEND_GROUP_MESSAGE_PATH]({ + group_openid: "g", + msg_type: 0, + content: "", + }), + "INVALID_ARGUMENT", + grpcStatus.INVALID_ARGUMENT, + ); + + setFetch(async () => response(429, JSON.stringify({ + code: 22009, + message: "msg limit exceed", + }))); + + const err = await expectGrpcError( + () => rpcdef(buildCtx({ secret: { accessToken: "token" } }))[METHOD_SEND_C2C_MESSAGE_PATH]({ openid: "1", content: "x" }), + "UNAVAILABLE", + grpcStatus.UNAVAILABLE, + ); + assert.equal(err.httpStatus, 429); + assert.equal(err.responseCode, 22009); +}); + +test("helpers cover token cache, invalid JSON, and URL normalization", async () => { + assert.equal(_test.buildApiUrl("https://api.sgroup.qq.com", "v2/users/u/messages"), "https://api.sgroup.qq.com/v2/users/u/messages"); + assert.equal(_test.normalizeBaseUrl("https://api.sgroup.qq.com/", "", "baseUrl"), "https://api.sgroup.qq.com"); + assert.equal(_test.resolveTimeoutMs({ bindings: { timeout_ms: "1234" } }), 1234); + assert.throws(() => _test.buildJsonObject("[1]", "markdown_json"), /JSON object/); + assert.throws(() => _test.parseJsonBody("{"), /not valid JSON/); + + const key = _test.tokenCacheKey("url", "app", "secret"); + _test.writeTokenCache(key, "cached", 7200, 1000); + assert.equal(_test.readTokenCache(key, 1000), "cached"); + assert.equal(_test.readTokenCache(key, 7200 * 1000), ""); +}); From a3aa3316c44a27d93d69509827e2ffbca3b4f611 Mon Sep 17 00:00:00 2001 From: git Date: Mon, 29 Jun 2026 15:33:07 +0800 Subject: [PATCH 2/6] docs: add Tencent QQ Bot OpenAPI v2 verification screenshot --- ...ent-qq-bot-openapi-v2-chat-verification.png | Bin 0 -> 129887 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/assets/tencent-qq-bot-openapi-v2-chat-verification.png diff --git a/docs/assets/tencent-qq-bot-openapi-v2-chat-verification.png b/docs/assets/tencent-qq-bot-openapi-v2-chat-verification.png new file mode 100644 index 0000000000000000000000000000000000000000..db5f4b69815303d2f3c13c84d387a97d86cb7c49 GIT binary patch literal 129887 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�^+d; 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

+x@_i-g^N-jYkvmxVg8dJJQf)1k(Z>$ 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

  • ktj_E<|gOb3gONSDx?aRI1426byC3nk>$Y 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 Date: Mon, 29 Jun 2026 17:52:17 +0800 Subject: [PATCH 3/6] Fix QQ Bot gateway timeout and reconnect handling --- .../tencent__qq-chat/src/tencent-qq-chat.js | 43 +++++++++--- .../test/tencent-qq-chat.test.js | 69 +++++++++++++++++++ 2 files changed, 103 insertions(+), 9 deletions(-) diff --git a/services/tencent__qq-chat/src/tencent-qq-chat.js b/services/tencent__qq-chat/src/tencent-qq-chat.js index 0a441042..a3e8aacc 100644 --- a/services/tencent__qq-chat/src/tencent-qq-chat.js +++ b/services/tencent__qq-chat/src/tencent-qq-chat.js @@ -51,6 +51,7 @@ const gatewayState = { reconnectTimer: null, ctx: null, localMessageSeq: 0, + connectionId: 0, }; const grpcCodeFor = (code) => ({ @@ -132,6 +133,19 @@ const resolveTimeoutMs = (ctx = {}) => { return optionalUint32(ctx.limits?.timeoutMs) ?? optionalUint32(bindings.timeoutMs) ?? optionalUint32(bindings.timeout_ms) ?? DEFAULT_TIMEOUT_MS; }; +const createTimeoutSignal = (timeoutMs) => { + if (typeof globalThis.AbortSignal?.timeout === "function") return globalThis.AbortSignal.timeout(timeoutMs); + const controller = new globalThis.AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + timer.unref?.(); + return controller.signal; +}; + +const fetchWithTimeout = (url, init = {}, timeoutMs = DEFAULT_TIMEOUT_MS) => fetch(url, { + ...init, + signal: init.signal ?? createTimeoutSignal(timeoutMs), +}); + const optionalIntWithDefault = (value, fallback, minimum = 0) => { const parsed = optionalUint32(value); if (parsed === undefined || parsed < minimum) return fallback; @@ -259,15 +273,14 @@ const fetchAppAccessToken = async (ctx = {}, forceRefresh = false) => { let response; try { - response = await fetch(tokenUrl, { + response = await fetchWithTimeout(tokenUrl, { method: "POST", - timeoutMs: resolveTimeoutMs(callCtx), headers: { "content-type": "application/json", accept: "application/json, */*;q=0.8", }, body: JSON.stringify({ appId, clientSecret: appSecret }), - }); + }, resolveTimeoutMs(callCtx)); } catch (err) { throw upstreamError("UNAVAILABLE", "qq bot access token request failed", { httpStatus: 0, @@ -333,8 +346,10 @@ const clearGatewayTimers = () => { const closeGatewaySocket = () => { const ws = gatewayState.ws; gatewayState.ws = null; + clearGatewayTimers(); if (!ws) return; try { + ws.__octobusIntentionalClose = true; ws.close(); } catch { // Ignore best-effort close errors. @@ -368,14 +383,13 @@ const enqueueGatewayMessage = async (payload) => { const fetchGatewayInfo = async (ctx = {}, accessToken) => { const baseUrl = resolveApiBaseUrl(resolveCallContext(ctx).bindings || {}); - const response = await fetch(buildApiUrl(baseUrl, "gateway/bot"), { + const response = await fetchWithTimeout(buildApiUrl(baseUrl, "gateway/bot"), { method: "GET", - timeoutMs: resolveTimeoutMs(ctx), headers: { authorization: `QQBot ${accessToken}`, accept: "application/json, */*;q=0.8", }, - }); + }, resolveTimeoutMs(ctx)); const httpStatus = Number(response.status || 0); const httpBody = String((await response.text()) ?? ""); const body = parseJsonBody(httpBody, "gateway response"); @@ -461,6 +475,8 @@ const handleGatewayPayload = async (payload, accessToken) => { const connectGateway = async () => { if (!gatewayState.running || !gatewayState.ctx) return; + const connectionId = gatewayState.connectionId + 1; + gatewayState.connectionId = connectionId; gatewayState.state = "connecting"; gatewayState.ready = false; gatewayState.lastError = ""; @@ -480,11 +496,13 @@ const connectGateway = async () => { gatewayState.ws = ws; ws.addEventListener("open", () => { + if (gatewayState.ws !== ws || gatewayState.connectionId !== connectionId) return; gatewayState.state = "open"; }); ws.addEventListener("message", (event) => { Promise.resolve() .then(async () => { + if (gatewayState.ws !== ws || gatewayState.connectionId !== connectionId) return; const payload = JSON.parse(String(event.data)); await handleGatewayPayload(payload, accessToken); }) @@ -493,11 +511,19 @@ const connectGateway = async () => { }); }); ws.addEventListener("error", (event) => { + if (gatewayState.ws !== ws || gatewayState.connectionId !== connectionId) return; gatewayState.lastError = event?.message || "gateway websocket error"; }); ws.addEventListener("close", () => { + const intentional = ws.__octobusIntentionalClose === true; + if (gatewayState.ws !== ws || gatewayState.connectionId !== connectionId) return; + gatewayState.ws = null; gatewayState.ready = false; clearGatewayTimers(); + if (intentional) { + if (!gatewayState.running) gatewayState.state = "stopped"; + return; + } if (gatewayState.running) scheduleGatewayReconnect(); else gatewayState.state = "stopped"; }); @@ -608,16 +634,15 @@ const callOpenApi = async (ctx = {}, path, payload = {}) => { let response; try { - response = await fetch(url, { + response = await fetchWithTimeout(url, { method: "POST", - timeoutMs: resolveTimeoutMs(callCtx), headers: { authorization: `QQBot ${accessToken}`, "content-type": "application/json", accept: "application/json, */*;q=0.8", }, body: JSON.stringify(payload), - }); + }, resolveTimeoutMs(callCtx)); } catch (err) { throw upstreamError("UNAVAILABLE", "qq bot openapi request failed", { httpStatus: 0, diff --git a/services/tencent__qq-chat/test/tencent-qq-chat.test.js b/services/tencent__qq-chat/test/tencent-qq-chat.test.js index e64ec99c..76474220 100644 --- a/services/tencent__qq-chat/test/tencent-qq-chat.test.js +++ b/services/tencent__qq-chat/test/tencent-qq-chat.test.js @@ -93,6 +93,8 @@ test("GetAccessToken posts AppID and AppSecret to official token endpoint", asyn assert.equal(captured.url, "https://bots.qq.com/app/getAppAccessToken"); assert.equal(captured.init.method, "POST"); + assert.ok(captured.init.signal instanceof AbortSignal); + assert.equal("timeoutMs" in captured.init, false); assert.deepEqual(JSON.parse(captured.init.body), { appId: "app-1", clientSecret: "secret-1", @@ -126,7 +128,11 @@ test("SendC2CMessage fetches token and posts to /v2/users/{openid}/messages", as }); assert.equal(calls.length, 2); + assert.ok(calls[0].init.signal instanceof AbortSignal); + assert.equal("timeoutMs" in calls[0].init, false); assert.equal(calls[1].url, "https://api.sgroup.qq.com/v2/users/USER_OPENID/messages"); + assert.ok(calls[1].init.signal instanceof AbortSignal); + assert.equal("timeoutMs" in calls[1].init, false); assert.equal(calls[1].init.headers.authorization, "QQBot access-1"); assert.deepEqual(JSON.parse(calls[1].init.body), { msg_type: 0, @@ -280,6 +286,8 @@ test("Gateway receiver starts, buffers messages, polls, and acks", async () => { const start = await rpcdef(ctx)[METHOD_START_GATEWAY_PATH]({}); assert.equal(start.running, true); assert.equal(fetchCalls[0].url, "https://api.sgroup.qq.com/gateway/bot"); + assert.ok(fetchCalls[0].init.signal instanceof AbortSignal); + assert.equal("timeoutMs" in fetchCalls[0].init, false); const socket = FakeWebSocket.instances[0]; socket.emit("message", { @@ -342,6 +350,67 @@ test("Gateway receiver starts, buffers messages, polls, and acks", async () => { assert.equal(stopped.running, false); }); +test("Gateway ignores stale close events after reconnecting a newer socket", async () => { + setFetch(async () => response(200, JSON.stringify({ + url: "wss://api.sgroup.qq.com/websocket", + shards: 1, + }))); + + class FakeWebSocket { + static OPEN = 1; + static instances = []; + + constructor(url) { + this.url = url; + this.readyState = FakeWebSocket.OPEN; + this.listeners = new Map(); + FakeWebSocket.instances.push(this); + queueMicrotask(() => this.emit("open", {})); + } + + addEventListener(name, callback) { + const listeners = this.listeners.get(name) || []; + listeners.push(callback); + this.listeners.set(name, listeners); + } + + send() {} + + close() { + this.readyState = 3; + queueMicrotask(() => this.emit("close", {})); + } + + emit(name, event) { + for (const callback of this.listeners.get(name) || []) callback(event); + } + } + + globalThis.WebSocket = FakeWebSocket; + + const ctx = buildCtx({ + secret: { accessToken: "gateway-token" }, + config: { + baseUrl: "https://api.sgroup.qq.com", + gatewayReconnectMs: 100, + }, + }); + + await rpcdef(ctx)[METHOD_START_GATEWAY_PATH]({}); + const firstSocket = FakeWebSocket.instances[0]; + assert.equal(_test.gatewayState.ws, firstSocket); + + await _test.connectGateway(); + const secondSocket = FakeWebSocket.instances[1]; + assert.equal(FakeWebSocket.instances.length, 2); + assert.equal(_test.gatewayState.ws, secondSocket); + + await new Promise((resolve) => setTimeout(resolve, 0)); + assert.equal(_test.gatewayState.ws, secondSocket); + assert.equal(_test.gatewayState.reconnectTimer, null); + assert.equal(_test.gatewayState.reconnectCount, 0); +}); + test("SDK handlers read ctx.request", async () => { setFetch(async () => response(200, JSON.stringify({ id: "msg-sdk", From 7172291515c3349ce8472b1b5ae96b76cee61bb9 Mon Sep 17 00:00:00 2001 From: git Date: Wed, 1 Jul 2026 17:57:57 +0800 Subject: [PATCH 4/6] Remove shared registry updates from QQ chat PR --- services/bin/octobus-tentacles.js | 68 +++++++++++++++++++++++++++++-- services/package.json | 57 +++++++++++++++++++++++--- 2 files changed, 116 insertions(+), 9 deletions(-) diff --git a/services/bin/octobus-tentacles.js b/services/bin/octobus-tentacles.js index 1c2ddec9..4124b804 100755 --- a/services/bin/octobus-tentacles.js +++ b/services/bin/octobus-tentacles.js @@ -17,6 +17,10 @@ const services = { entryFile: "../chaitin__safeline-waf-eliminate-false-positive/bin/safeline-waf-eliminate-false-positive.js", serviceModule: "../chaitin__safeline-waf-eliminate-false-positive/src/service.js", }, + "cloudatlas": { + entryFile: "../chaitin__cloudatlas/bin/cloudatlas.js", + serviceModule: "../chaitin__cloudatlas/src/service.js", + }, "das-gateway-v3": { entryFile: "../das__gateway_v3/bin/das-gateway-v3.js", serviceModule: "../das__gateway_v3/src/service.js", @@ -25,6 +29,10 @@ const services = { entryFile: "../das__tgfw_v6/bin/das-tgfw-v6.js", serviceModule: "../das__tgfw_v6/src/service.js", }, + "dbaudit": { + entryFile: "../das__dbaudit/bin/dbaudit.js", + serviceModule: "../das__dbaudit/src/service.js", + }, "defectdojo": { entryFile: "../defectdojo__defectdojo/bin/defectdojo.js", serviceModule: "../defectdojo__defectdojo/src/service.js", @@ -53,6 +61,10 @@ const services = { entryFile: "../feishu__group-robot/bin/feishu-group-robot.js", serviceModule: "../feishu__group-robot/src/service.js", }, + "first-epss-v1": { + entryFile: "../first__epss-v1/bin/first-epss-v1.js", + serviceModule: "../first__epss-v1/src/service.js", + }, "fortinet-fw": { entryFile: "../fortinet__fw/bin/fortinet-fw.js", serviceModule: "../fortinet__fw/src/service.js", @@ -61,6 +73,10 @@ const services = { entryFile: "../fortinet__waf/bin/fortinet-waf.js", serviceModule: "../fortinet__waf/src/service.js", }, + "hermes-gateway": { + entryFile: "../hermes__gateway/bin/hermes-gateway.js", + serviceModule: "../hermes__gateway/src/service.js", + }, "hillstone-fw-v5-5-r10": { entryFile: "../hillstone__fw_v5-5-r10/bin/hillstone-fw-v5-5-r10.js", serviceModule: "../hillstone__fw_v5-5-r10/src/service.js", @@ -73,6 +89,10 @@ const services = { entryFile: "../hillstone__fw_v5-5-r6/bin/hillstone-fw-v5-5-r6.js", serviceModule: "../hillstone__fw_v5-5-r6/src/service.js", }, + "huorong-endpoint-security-management-system-v2-0-19-3": { + entryFile: "../huorong__endpoint-security-management-system_v2-0-19-3/bin/huorong-endpoint-security-management-system-v2-0-19-3.js", + serviceModule: "../huorong__endpoint-security-management-system_v2-0-19-3/src/service.js", + }, "huawei-fw-usg6000e": { entryFile: "../huawei__fw-usg6000e/bin/huawei-fw-usg6000e.js", serviceModule: "../huawei__fw-usg6000e/src/service.js", @@ -89,6 +109,10 @@ const services = { entryFile: "../nsfocus__nips_v5-6-r11/bin/nsfocus-nips-v5-6-r11.js", serviceModule: "../nsfocus__nips_v5-6-r11/src/service.js", }, + "owasp-dependency-track-sca-v5-0": { + entryFile: "../owasp__dependency-track-sca_v5-0/bin/owasp-dependency-track-sca-v5-0.js", + serviceModule: "../owasp__dependency-track-sca_v5-0/src/service.js", + }, "panabit-tang-r1": { entryFile: "../panabit__tang-r1/bin/panabit-tang-r1.js", serviceModule: "../panabit__tang-r1/src/service.js", @@ -101,6 +125,10 @@ const services = { entryFile: "../qianxin__fw-secgate3600-http-x/bin/qianxin-fw-secgate3600-http-x.js", serviceModule: "../qianxin__fw-secgate3600-http-x/src/service.js", }, + "qianxin-hunter": { + entryFile: "../qianxin__hunter_v23-1/bin/qianxin-hunter.js", + serviceModule: "../qianxin__hunter_v23-1/src/service.js", + }, "qiming-tianqing-waf": { entryFile: "../qiming-tianqing__waf/bin/qiming-tianqing-waf.js", serviceModule: "../qiming-tianqing__waf/src/service.js", @@ -109,6 +137,10 @@ const services = { entryFile: "../qingteng__hids_v3-4/bin/qingteng-hids-v3-4.js", serviceModule: "../qingteng__hids_v3-4/src/service.js", }, + "qingteng-hids-v5": { + entryFile: "../qingteng__hids_v5/bin/qingteng-hids-v5.js", + serviceModule: "../qingteng__hids_v5/src/service.js", + }, "ray-waf-v6-1-2": { entryFile: "../ray__waf_v6-1-2/bin/ray-waf-v6-1-2.js", serviceModule: "../ray__waf_v6-1-2/src/service.js", @@ -133,18 +165,22 @@ const services = { entryFile: "../skycloud__inet/bin/skycloud-inet.js", serviceModule: "../skycloud__inet/src/service.js", }, - "tencent-qq-chat": { - entryFile: "../tencent__qq-chat/bin/tencent-qq-chat.js", - serviceModule: "../tencent__qq-chat/src/service.js", - }, "tencent-qyweixin-group-robot": { entryFile: "../tencent__qyweixin-group-robot/bin/tencent-qyweixin-group-robot.js", serviceModule: "../tencent__qyweixin-group-robot/src/service.js", }, + "tencent-tix-saas": { + entryFile: "../tencent__tix-saas/bin/tencent-tix-saas.js", + serviceModule: "../tencent__tix-saas/src/service.js", + }, "tencent-tsec-v2-5-1": { entryFile: "../tencent__tsec_v2-5-1/bin/tencent-tsec-v2-5-1.js", serviceModule: "../tencent__tsec_v2-5-1/src/service.js", }, + "telegram-bot-api": { + entryFile: "../telegram__bot-api/bin/telegram-bot-api.js", + serviceModule: "../telegram__bot-api/src/service.js", + }, "threatbook-cloudapi-v3": { entryFile: "../threatbook__cloudapi_v3/bin/threatbook-cloudapi-v3.js", serviceModule: "../threatbook__cloudapi_v3/src/service.js", @@ -153,6 +189,10 @@ const services = { entryFile: "../threatbook__claudsandbox_v3/bin/threatbook-claudsandbox-v3.js", serviceModule: "../threatbook__claudsandbox_v3/src/service.js", }, + "threatbook-ngtip-v5": { + entryFile: "../threatbook__ngtip_v5/bin/threatbook-ngtip-v5.js", + serviceModule: "../threatbook__ngtip_v5/src/service.js", + }, "threatbook-onesig": { entryFile: "../threatbook__onesig/bin/threatbook-onesig.js", serviceModule: "../threatbook__onesig/src/service.js", @@ -185,6 +225,10 @@ const services = { entryFile: "../venus__ads_v3-6/bin/venus-ads-v3-6.js", serviceModule: "../venus__ads_v3-6/src/service.js", }, + "volcengine-cloud-firewall": { + entryFile: "../volcengine__cloud-firewall/bin/volcengine-cloud-firewall.js", + serviceModule: "../volcengine__cloud-firewall/src/service.js", + }, "wangsu-label-ip": { entryFile: "../wangsu__label-ip/bin/wangsu-label-ip.js", serviceModule: "../wangsu__label-ip/src/service.js", @@ -193,6 +237,22 @@ const services = { entryFile: "../wd__k01/bin/wd-k01.js", serviceModule: "../wd__k01/src/service.js", }, + "tencent-bh": { + entryFile: "../tencent__bh/bin/tencent-bh.js", + serviceModule: "../tencent__bh/src/service.js", + }, + "alibaba-sas": { + entryFile: "../alibaba__sas/bin/alibaba-sas.js", + serviceModule: "../alibaba__sas/src/service.js", + }, + "misp": { + entryFile: "../misp__misp/bin/misp.js", + serviceModule: "../misp__misp/src/service.js", + }, + "opencti": { + entryFile: "../filigran__opencti/bin/opencti.js", + serviceModule: "../filigran__opencti/src/service.js", + }, }; const serviceNames = Object.keys(services); diff --git a/services/package.json b/services/package.json index f949fef0..336563c7 100644 --- a/services/package.json +++ b/services/package.json @@ -6,8 +6,10 @@ "bin": { "octobus-tentacles": "bin/octobus-tentacles.js", "alibaba-cloud-simple-application-server-firewall": "bin/alibaba-cloud-simple-application-server-firewall.js", + "cloudatlas": "bin/cloudatlas.js", "das-gateway-v3": "bin/das-gateway-v3.js", "das-tgfw-v6": "bin/das-tgfw-v6.js", + "dbaudit": "bin/dbaudit.js", "defectdojo": "bin/defectdojo.js", "dingtalk-group-robot": "bin/dingtalk-group-robot.js", "dptech-eds": "bin/dptech-eds.js", @@ -15,20 +17,26 @@ "dptech-umc-ads-v5-3-29": "bin/dptech-umc-ads-v5-3-29.js", "elastic-kibana-7-17-26": "bin/elastic-kibana-7-17-26.js", "feishu-group-robot": "bin/feishu-group-robot.js", + "first-epss-v1": "bin/first-epss-v1.js", "fortinet-fw": "bin/fortinet-fw.js", "fortinet-waf": "bin/fortinet-waf.js", + "hermes-gateway": "bin/hermes-gateway.js", "hillstone-fw-v5-5-r4": "bin/hillstone-fw-v5-5-r4.js", "hillstone-fw-v5-5-r6": "bin/hillstone-fw-v5-5-r6.js", "hillstone-fw-v5-5-r10": "bin/hillstone-fw-v5-5-r10.js", + "huorong-endpoint-security-management-system-v2-0-19-3": "bin/huorong-endpoint-security-management-system-v2-0-19-3.js", "huawei-fw-usg6000e": "bin/huawei-fw-usg6000e.js", "imperva-waf-gateway-v13-6-90": "bin/imperva-waf-gateway-v13-6-90.js", "nsfocus-ads-v4-5-r90-f06": "bin/nsfocus-ads-v4-5-r90-f06.js", "nsfocus-nips-v5-6-r11": "bin/nsfocus-nips-v5-6-r11.js", + "owasp-dependency-track-sca-v5-0": "bin/owasp-dependency-track-sca-v5-0.js", "panabit-tang-r1": "bin/panabit-tang-r1.js", "qianxin-fw-secgate3600": "bin/qianxin-fw-secgate3600.js", "qianxin-fw-secgate3600-http-x": "bin/qianxin-fw-secgate3600-http-x.js", + "qianxin-hunter": "bin/qianxin-hunter.js", "qiming-tianqing-waf": "bin/qiming-tianqing-waf.js", "qingteng-hids-v3-4": "bin/qingteng-hids-v3-4.js", + "qingteng-hids-v5": "bin/qingteng-hids-v5.js", "ray-waf-v6-1-2": "bin/ray-waf-v6-1-2.js", "riversafe-waf": "bin/riversafe-waf.js", "safeline-waf": "bin/safeline-waf.js", @@ -38,10 +46,12 @@ "skycloud-inet": "bin/skycloud-inet.js", "slack-group-robot": "bin/slack-group-robot.js", "tencent-qyweixin-group-robot": "bin/tencent-qyweixin-group-robot.js", - "tencent-qq-chat": "bin/tencent-qq-chat.js", + "tencent-tix-saas": "bin/tencent-tix-saas.js", "tencent-tsec-v2-5-1": "bin/tencent-tsec-v2-5-1.js", + "telegram-bot-api": "bin/telegram-bot-api.js", "threatbook-cloudapi-v3": "bin/threatbook-cloudapi-v3.js", "threatbook-claudsandbox-v3": "bin/threatbook-claudsandbox-v3.js", + "threatbook-ngtip-v5": "bin/threatbook-ngtip-v5.js", "threatbook-onesig": "bin/threatbook-onesig.js", "threatbook-tdp": "bin/threatbook-tdp.js", "threatbook-tip-v4": "bin/threatbook-tip-v4.js", @@ -50,13 +60,20 @@ "topsec-fw-2u": "bin/topsec-fw-2u.js", "topsec-waf-v3-2294-20238": "bin/topsec-waf-v3-2294-20238.js", "venus-ads-v3-6": "bin/venus-ads-v3-6.js", + "volcengine-cloud-firewall": "bin/volcengine-cloud-firewall.js", "wangsu-label-ip": "bin/wangsu-label-ip.js", - "wd-k01": "bin/wd-k01.js" + "wd-k01": "bin/wd-k01.js", + "tencent-bh": "bin/tencent-bh.js", + "alibaba-sas": "bin/alibaba-sas.js", + "misp": "bin/misp.js", + "opencti": "bin/opencti.js" }, "files": [ "bin/alibaba-cloud-simple-application-server-firewall.js", + "bin/cloudatlas.js", "bin/das-gateway-v3.js", "bin/das-tgfw-v6.js", + "bin/dbaudit.js", "bin/defectdojo.js", "bin/dingtalk-group-robot.js", "bin/dptech-eds.js", @@ -64,20 +81,26 @@ "bin/dptech-umc-ads-v5-3-29.js", "bin/elastic-kibana-7-17-26.js", "bin/feishu-group-robot.js", + "bin/first-epss-v1.js", "bin/fortinet-fw.js", "bin/fortinet-waf.js", + "bin/hermes-gateway.js", "bin/hillstone-fw-v5-5-r4.js", "bin/hillstone-fw-v5-5-r6.js", "bin/hillstone-fw-v5-5-r10.js", + "bin/huorong-endpoint-security-management-system-v2-0-19-3.js", "bin/huawei-fw-usg6000e.js", "bin/imperva-waf-gateway-v13-6-90.js", "bin/nsfocus-ads-v4-5-r90-f06.js", "bin/nsfocus-nips-v5-6-r11.js", + "bin/owasp-dependency-track-sca-v5-0.js", "bin/panabit-tang-r1.js", "bin/qianxin-fw-secgate3600.js", "bin/qianxin-fw-secgate3600-http-x.js", + "bin/qianxin-hunter.js", "bin/qiming-tianqing-waf.js", "bin/qingteng-hids-v3-4.js", + "bin/qingteng-hids-v5.js", "bin/ray-waf-v6-1-2.js", "bin/riversafe-waf.js", "bin/skycloud-inet.js", @@ -86,11 +109,13 @@ "bin/safeline-waf.js", "bin/sangfor-fw-v8-0-45.js", "bin/sangfor-xdr-v2-0-45.js", - "bin/tencent-qq-chat.js", "bin/tencent-qyweixin-group-robot.js", + "bin/tencent-tix-saas.js", "bin/tencent-tsec-v2-5-1.js", + "bin/telegram-bot-api.js", "bin/threatbook-cloudapi-v3.js", "bin/threatbook-claudsandbox-v3.js", + "bin/threatbook-ngtip-v5.js", "bin/threatbook-onesig.js", "bin/threatbook-tdp.js", "bin/threatbook-tip-v4.js", @@ -99,11 +124,18 @@ "bin/topsec-fw-v3-7-6.js", "bin/topsec-waf-v3-2294-20238.js", "bin/venus-ads-v3-6.js", + "bin/volcengine-cloud-firewall.js", "bin/wd-k01.js", + "bin/tencent-bh.js", + "bin/alibaba-sas.js", + "bin/misp.js", + "bin/opencti.js", "bin/wangsu-label-ip.js", "alibaba-cloud__simple-application-server-firewall", + "chaitin__cloudatlas", "das__gateway_v3", "das__tgfw_v6", + "das__dbaudit", "defectdojo__defectdojo", "dingtalk__group-robot", "dptech__eds", @@ -111,20 +143,26 @@ "dptech__umc-ads_v5-3-29", "elastic__kibana_7-17-26", "feishu__group-robot", + "first__epss-v1", "fortinet__fw", "fortinet__waf", + "hermes__gateway", "hillstone__fw_v5-5-r4", "hillstone__fw_v5-5-r6", "hillstone__fw_v5-5-r10", + "huorong__endpoint-security-management-system_v2-0-19-3", "huawei__fw-usg6000e", "imperva__waf-gateway_v13-6-90", "nsfocus__ads_v4-5-r90-f06", "nsfocus__nips_v5-6-r11", + "owasp__dependency-track-sca_v5-0", "panabit__tang-r1", "qianxin__fw-secgate3600", "qianxin__fw-secgate3600-http-x", + "qianxin__hunter_v23-1", "qiming-tianqing__waf", "qingteng__hids_v3-4", + "qingteng__hids_v5", "ray__waf_v6-1-2", "riversafe__waf", "skycloud__inet", @@ -133,11 +171,13 @@ "chaitin__safeline-waf", "sangfor__fw_v8-0-45", "sangfor__xdr_v2-0-45", - "tencent__qq-chat", "tencent__qyweixin-group-robot", + "tencent__tix-saas", "tencent__tsec_v2-5-1", + "telegram__bot-api", "threatbook__cloudapi_v3", "threatbook__claudsandbox_v3", + "threatbook__ngtip_v5", "threatbook__onesig", "threatbook__tdp", "threatbook__tip_v4", @@ -146,12 +186,19 @@ "topsec__fw_v3-7-6", "topsec__waf_v3-2294-20238", "venus__ads_v3-6", + "volcengine__cloud-firewall", "wd__k01", + "tencent__bh", + "alibaba__sas", + "misp__misp", + "filigran__opencti", "wangsu__label-ip", "scripts", "bin/octobus-tentacles.js" ], "scripts": { + "registry:generate": "node scripts/generate-service-registry.mjs", + "registry:check": "node scripts/generate-service-registry.mjs --check", "validate": "node scripts/validate-service-package.mjs", "test": "node scripts/run-tests.mjs", "coverage:all": "node scripts/run-coverage-all.mjs", @@ -160,7 +207,7 @@ }, "dependencies": { "@alicloud/swas-open20200601": "^4.0.0", - "@chaitin-ai/octobus-sdk": "^0.5.0", + "@chaitin-ai/octobus-sdk": "^0.6.0", "commander": "^12.1.0", "undici": "^7.16.0" }, From 860671ff7002fa7721c7ff936a6aba0448601370 Mon Sep 17 00:00:00 2001 From: git Date: Wed, 1 Jul 2026 18:00:29 +0800 Subject: [PATCH 5/6] Remove registry script changes from QQ chat PR --- services/package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/services/package.json b/services/package.json index 336563c7..2fbea088 100644 --- a/services/package.json +++ b/services/package.json @@ -197,8 +197,6 @@ "bin/octobus-tentacles.js" ], "scripts": { - "registry:generate": "node scripts/generate-service-registry.mjs", - "registry:check": "node scripts/generate-service-registry.mjs --check", "validate": "node scripts/validate-service-package.mjs", "test": "node scripts/run-tests.mjs", "coverage:all": "node scripts/run-coverage-all.mjs", From 895ca3bae8269c054f5ca5c848518fe74152ca3f Mon Sep 17 00:00:00 2001 From: git Date: Wed, 1 Jul 2026 18:19:09 +0800 Subject: [PATCH 6/6] chore(services): generate service registry --- services/bin/dsensor.js | 10 + services/bin/fofa-network-space-mapper.js | 10 + services/bin/octobus-tentacles.js | 92 ++-- services/bin/opencti.js | 0 .../bin/ruijie-behavior-firewall-r2-3-2-t0.js | 10 + services/bin/volcengine-cloud-firewall.js | 0 .../bin/fofa-network-space-mapper.js | 0 services/package.json | 76 ++-- .../scripts/generate-service-registry.mjs | 410 ++++++++++++++++++ .../tests/validate-service-package.test.mjs | 14 + 10 files changed, 546 insertions(+), 76 deletions(-) create mode 100755 services/bin/dsensor.js create mode 100755 services/bin/fofa-network-space-mapper.js mode change 100644 => 100755 services/bin/opencti.js create mode 100755 services/bin/ruijie-behavior-firewall-r2-3-2-t0.js mode change 100644 => 100755 services/bin/volcengine-cloud-firewall.js mode change 100644 => 100755 services/fofa__network-space-mapper/bin/fofa-network-space-mapper.js create mode 100644 services/scripts/generate-service-registry.mjs diff --git a/services/bin/dsensor.js b/services/bin/dsensor.js new file mode 100755 index 00000000..76e9e4e0 --- /dev/null +++ b/services/bin/dsensor.js @@ -0,0 +1,10 @@ +#!/usr/bin/env node + +import { fileURLToPath } from "node:url"; +import { runServiceMain } from "@chaitin-ai/octobus-sdk"; + +import { service } from "../chaitin__dsensor_ds-s_h_40-25.07.001/src/service.js"; + +runServiceMain(service, { + entryFile: fileURLToPath(new URL("../chaitin__dsensor_ds-s_h_40-25.07.001/bin/dsensor.js", import.meta.url)), +}); diff --git a/services/bin/fofa-network-space-mapper.js b/services/bin/fofa-network-space-mapper.js new file mode 100755 index 00000000..7ae4b55b --- /dev/null +++ b/services/bin/fofa-network-space-mapper.js @@ -0,0 +1,10 @@ +#!/usr/bin/env node + +import { fileURLToPath } from "node:url"; +import { runServiceMain } from "@chaitin-ai/octobus-sdk"; + +import { service } from "../fofa__network-space-mapper/src/service.js"; + +runServiceMain(service, { + entryFile: fileURLToPath(new URL("../fofa__network-space-mapper/bin/fofa-network-space-mapper.js", import.meta.url)), +}); diff --git a/services/bin/octobus-tentacles.js b/services/bin/octobus-tentacles.js index 4124b804..bb8ad04c 100755 --- a/services/bin/octobus-tentacles.js +++ b/services/bin/octobus-tentacles.js @@ -9,14 +9,6 @@ const services = { entryFile: "../alibaba-cloud__simple-application-server-firewall/bin/alibaba-cloud-simple-application-server-firewall.js", serviceModule: "../alibaba-cloud__simple-application-server-firewall/src/service.js", }, - "safeline-waf": { - entryFile: "../chaitin__safeline-waf/bin/safeline-waf.js", - serviceModule: "../chaitin__safeline-waf/src/service.js", - }, - "safeline-waf-eliminate-false-positive": { - entryFile: "../chaitin__safeline-waf-eliminate-false-positive/bin/safeline-waf-eliminate-false-positive.js", - serviceModule: "../chaitin__safeline-waf-eliminate-false-positive/src/service.js", - }, "cloudatlas": { entryFile: "../chaitin__cloudatlas/bin/cloudatlas.js", serviceModule: "../chaitin__cloudatlas/src/service.js", @@ -53,6 +45,10 @@ const services = { entryFile: "../dptech__umc-ads_v5-3-29/bin/dptech-umc-ads-v5-3-29.js", serviceModule: "../dptech__umc-ads_v5-3-29/src/service.js", }, + "dsensor": { + entryFile: "../chaitin__dsensor_ds-s_h_40-25.07.001/bin/dsensor.js", + serviceModule: "../chaitin__dsensor_ds-s_h_40-25.07.001/src/service.js", + }, "elastic-kibana-7-17-26": { entryFile: "../elastic__kibana_7-17-26/bin/elastic-kibana-7-17-26.js", serviceModule: "../elastic__kibana_7-17-26/src/service.js", @@ -65,6 +61,10 @@ const services = { entryFile: "../first__epss-v1/bin/first-epss-v1.js", serviceModule: "../first__epss-v1/src/service.js", }, + "fofa-network-space-mapper": { + entryFile: "../fofa__network-space-mapper/bin/fofa-network-space-mapper.js", + serviceModule: "../fofa__network-space-mapper/src/service.js", + }, "fortinet-fw": { entryFile: "../fortinet__fw/bin/fortinet-fw.js", serviceModule: "../fortinet__fw/src/service.js", @@ -89,14 +89,14 @@ const services = { entryFile: "../hillstone__fw_v5-5-r6/bin/hillstone-fw-v5-5-r6.js", serviceModule: "../hillstone__fw_v5-5-r6/src/service.js", }, - "huorong-endpoint-security-management-system-v2-0-19-3": { - entryFile: "../huorong__endpoint-security-management-system_v2-0-19-3/bin/huorong-endpoint-security-management-system-v2-0-19-3.js", - serviceModule: "../huorong__endpoint-security-management-system_v2-0-19-3/src/service.js", - }, "huawei-fw-usg6000e": { entryFile: "../huawei__fw-usg6000e/bin/huawei-fw-usg6000e.js", serviceModule: "../huawei__fw-usg6000e/src/service.js", }, + "huorong-endpoint-security-management-system-v2-0-19-3": { + entryFile: "../huorong__endpoint-security-management-system_v2-0-19-3/bin/huorong-endpoint-security-management-system-v2-0-19-3.js", + serviceModule: "../huorong__endpoint-security-management-system_v2-0-19-3/src/service.js", + }, "imperva-waf-gateway-v13-6-90": { entryFile: "../imperva__waf-gateway_v13-6-90/bin/imperva-waf-gateway-v13-6-90.js", serviceModule: "../imperva__waf-gateway_v13-6-90/src/service.js", @@ -105,10 +105,18 @@ const services = { entryFile: "../nsfocus__ads_v4-5-r90-f06/bin/nsfocus-ads-v4-5-r90-f06.js", serviceModule: "../nsfocus__ads_v4-5-r90-f06/src/service.js", }, + "nsfocus-ngfw-v60-9900": { + entryFile: "../nsfocus__ngfw_v60-9900/bin/nsfocus-ngfw-v60-9900.js", + serviceModule: "../nsfocus__ngfw_v60-9900/src/service.js", + }, "nsfocus-nips-v5-6-r11": { entryFile: "../nsfocus__nips_v5-6-r11/bin/nsfocus-nips-v5-6-r11.js", serviceModule: "../nsfocus__nips_v5-6-r11/src/service.js", }, + "opencti": { + entryFile: "../filigran__opencti/bin/opencti.js", + serviceModule: "../filigran__opencti/src/service.js", + }, "owasp-dependency-track-sca-v5-0": { entryFile: "../owasp__dependency-track-sca_v5-0/bin/owasp-dependency-track-sca-v5-0.js", serviceModule: "../owasp__dependency-track-sca_v5-0/src/service.js", @@ -149,6 +157,18 @@ const services = { entryFile: "../riversafe__waf/bin/riversafe-waf.js", serviceModule: "../riversafe__waf/src/service.js", }, + "ruijie-behavior-firewall-r2-3-2-t0": { + entryFile: "../ruijie__behavior_firewall_r2-3-2-t0/bin/ruijie-behavior-firewall-r2-3-2-t0.js", + serviceModule: "../ruijie__behavior_firewall_r2-3-2-t0/src/service.js", + }, + "safeline-waf": { + entryFile: "../chaitin__safeline-waf/bin/safeline-waf.js", + serviceModule: "../chaitin__safeline-waf/src/service.js", + }, + "safeline-waf-eliminate-false-positive": { + entryFile: "../chaitin__safeline-waf-eliminate-false-positive/bin/safeline-waf-eliminate-false-positive.js", + serviceModule: "../chaitin__safeline-waf-eliminate-false-positive/src/service.js", + }, "sangfor-fw-v8-0-45": { entryFile: "../sangfor__fw_v8-0-45/bin/sangfor-fw-v8-0-45.js", serviceModule: "../sangfor__fw_v8-0-45/src/service.js", @@ -157,13 +177,21 @@ const services = { entryFile: "../sangfor__xdr_v2-0-45/bin/sangfor-xdr-v2-0-45.js", serviceModule: "../sangfor__xdr_v2-0-45/src/service.js", }, + "skycloud-inet": { + entryFile: "../skycloud__inet/bin/skycloud-inet.js", + serviceModule: "../skycloud__inet/src/service.js", + }, "slack-group-robot": { entryFile: "../slack__group-robot/bin/slack-group-robot.js", serviceModule: "../slack__group-robot/src/service.js", }, - "skycloud-inet": { - entryFile: "../skycloud__inet/bin/skycloud-inet.js", - serviceModule: "../skycloud__inet/src/service.js", + "telegram-bot-api": { + entryFile: "../telegram__bot-api/bin/telegram-bot-api.js", + serviceModule: "../telegram__bot-api/src/service.js", + }, + "tencent-qq-chat": { + entryFile: "../tencent__qq-chat/bin/tencent-qq-chat.js", + serviceModule: "../tencent__qq-chat/src/service.js", }, "tencent-qyweixin-group-robot": { entryFile: "../tencent__qyweixin-group-robot/bin/tencent-qyweixin-group-robot.js", @@ -177,18 +205,14 @@ const services = { entryFile: "../tencent__tsec_v2-5-1/bin/tencent-tsec-v2-5-1.js", serviceModule: "../tencent__tsec_v2-5-1/src/service.js", }, - "telegram-bot-api": { - entryFile: "../telegram__bot-api/bin/telegram-bot-api.js", - serviceModule: "../telegram__bot-api/src/service.js", + "threatbook-claudsandbox-v3": { + entryFile: "../threatbook__claudsandbox_v3/bin/threatbook-claudsandbox-v3.js", + serviceModule: "../threatbook__claudsandbox_v3/src/service.js", }, "threatbook-cloudapi-v3": { entryFile: "../threatbook__cloudapi_v3/bin/threatbook-cloudapi-v3.js", serviceModule: "../threatbook__cloudapi_v3/src/service.js", }, - "threatbook-claudsandbox-v3": { - entryFile: "../threatbook__claudsandbox_v3/bin/threatbook-claudsandbox-v3.js", - serviceModule: "../threatbook__claudsandbox_v3/src/service.js", - }, "threatbook-ngtip-v5": { entryFile: "../threatbook__ngtip_v5/bin/threatbook-ngtip-v5.js", serviceModule: "../threatbook__ngtip_v5/src/service.js", @@ -205,10 +229,6 @@ const services = { entryFile: "../threatbook__tip_v4/bin/threatbook-tip-v4.js", serviceModule: "../threatbook__tip_v4/src/service.js", }, - "topsec-waf-v3-2294-20238": { - entryFile: "../topsec__waf_v3-2294-20238/bin/topsec-waf-v3-2294-20238.js", - serviceModule: "../topsec__waf_v3-2294-20238/src/service.js", - }, "topsec-fw-2u": { entryFile: "../topsec__fw-2u/bin/topsec-fw-2u.js", serviceModule: "../topsec__fw-2u/src/service.js", @@ -221,6 +241,10 @@ const services = { entryFile: "../topsec__fw_v3-7-6/bin/topsec-fw-v3-7-6.js", serviceModule: "../topsec__fw_v3-7-6/src/service.js", }, + "topsec-waf-v3-2294-20238": { + entryFile: "../topsec__waf_v3-2294-20238/bin/topsec-waf-v3-2294-20238.js", + serviceModule: "../topsec__waf_v3-2294-20238/src/service.js", + }, "venus-ads-v3-6": { entryFile: "../venus__ads_v3-6/bin/venus-ads-v3-6.js", serviceModule: "../venus__ads_v3-6/src/service.js", @@ -237,22 +261,6 @@ const services = { entryFile: "../wd__k01/bin/wd-k01.js", serviceModule: "../wd__k01/src/service.js", }, - "tencent-bh": { - entryFile: "../tencent__bh/bin/tencent-bh.js", - serviceModule: "../tencent__bh/src/service.js", - }, - "alibaba-sas": { - entryFile: "../alibaba__sas/bin/alibaba-sas.js", - serviceModule: "../alibaba__sas/src/service.js", - }, - "misp": { - entryFile: "../misp__misp/bin/misp.js", - serviceModule: "../misp__misp/src/service.js", - }, - "opencti": { - entryFile: "../filigran__opencti/bin/opencti.js", - serviceModule: "../filigran__opencti/src/service.js", - }, }; const serviceNames = Object.keys(services); diff --git a/services/bin/opencti.js b/services/bin/opencti.js old mode 100644 new mode 100755 diff --git a/services/bin/ruijie-behavior-firewall-r2-3-2-t0.js b/services/bin/ruijie-behavior-firewall-r2-3-2-t0.js new file mode 100755 index 00000000..b369f1b4 --- /dev/null +++ b/services/bin/ruijie-behavior-firewall-r2-3-2-t0.js @@ -0,0 +1,10 @@ +#!/usr/bin/env node + +import { fileURLToPath } from "node:url"; +import { runServiceMain } from "@chaitin-ai/octobus-sdk"; + +import { service } from "../ruijie__behavior_firewall_r2-3-2-t0/src/service.js"; + +runServiceMain(service, { + entryFile: fileURLToPath(new URL("../ruijie__behavior_firewall_r2-3-2-t0/bin/ruijie-behavior-firewall-r2-3-2-t0.js", import.meta.url)), +}); diff --git a/services/bin/volcengine-cloud-firewall.js b/services/bin/volcengine-cloud-firewall.js old mode 100644 new mode 100755 diff --git a/services/fofa__network-space-mapper/bin/fofa-network-space-mapper.js b/services/fofa__network-space-mapper/bin/fofa-network-space-mapper.js old mode 100644 new mode 100755 diff --git a/services/package.json b/services/package.json index 2fbea088..ce537ef7 100644 --- a/services/package.json +++ b/services/package.json @@ -15,20 +15,24 @@ "dptech-eds": "bin/dptech-eds.js", "dptech-fw-v4-6-10": "bin/dptech-fw-v4-6-10.js", "dptech-umc-ads-v5-3-29": "bin/dptech-umc-ads-v5-3-29.js", + "dsensor": "bin/dsensor.js", "elastic-kibana-7-17-26": "bin/elastic-kibana-7-17-26.js", "feishu-group-robot": "bin/feishu-group-robot.js", "first-epss-v1": "bin/first-epss-v1.js", + "fofa-network-space-mapper": "bin/fofa-network-space-mapper.js", "fortinet-fw": "bin/fortinet-fw.js", "fortinet-waf": "bin/fortinet-waf.js", "hermes-gateway": "bin/hermes-gateway.js", + "hillstone-fw-v5-5-r10": "bin/hillstone-fw-v5-5-r10.js", "hillstone-fw-v5-5-r4": "bin/hillstone-fw-v5-5-r4.js", "hillstone-fw-v5-5-r6": "bin/hillstone-fw-v5-5-r6.js", - "hillstone-fw-v5-5-r10": "bin/hillstone-fw-v5-5-r10.js", - "huorong-endpoint-security-management-system-v2-0-19-3": "bin/huorong-endpoint-security-management-system-v2-0-19-3.js", "huawei-fw-usg6000e": "bin/huawei-fw-usg6000e.js", + "huorong-endpoint-security-management-system-v2-0-19-3": "bin/huorong-endpoint-security-management-system-v2-0-19-3.js", "imperva-waf-gateway-v13-6-90": "bin/imperva-waf-gateway-v13-6-90.js", "nsfocus-ads-v4-5-r90-f06": "bin/nsfocus-ads-v4-5-r90-f06.js", + "nsfocus-ngfw-v60-9900": "bin/nsfocus-ngfw-v60-9900.js", "nsfocus-nips-v5-6-r11": "bin/nsfocus-nips-v5-6-r11.js", + "opencti": "bin/opencti.js", "owasp-dependency-track-sca-v5-0": "bin/owasp-dependency-track-sca-v5-0.js", "panabit-tang-r1": "bin/panabit-tang-r1.js", "qianxin-fw-secgate3600": "bin/qianxin-fw-secgate3600.js", @@ -39,34 +43,32 @@ "qingteng-hids-v5": "bin/qingteng-hids-v5.js", "ray-waf-v6-1-2": "bin/ray-waf-v6-1-2.js", "riversafe-waf": "bin/riversafe-waf.js", + "ruijie-behavior-firewall-r2-3-2-t0": "bin/ruijie-behavior-firewall-r2-3-2-t0.js", "safeline-waf": "bin/safeline-waf.js", "safeline-waf-eliminate-false-positive": "bin/safeline-waf-eliminate-false-positive.js", "sangfor-fw-v8-0-45": "bin/sangfor-fw-v8-0-45.js", "sangfor-xdr-v2-0-45": "bin/sangfor-xdr-v2-0-45.js", "skycloud-inet": "bin/skycloud-inet.js", "slack-group-robot": "bin/slack-group-robot.js", + "telegram-bot-api": "bin/telegram-bot-api.js", + "tencent-qq-chat": "bin/tencent-qq-chat.js", "tencent-qyweixin-group-robot": "bin/tencent-qyweixin-group-robot.js", "tencent-tix-saas": "bin/tencent-tix-saas.js", "tencent-tsec-v2-5-1": "bin/tencent-tsec-v2-5-1.js", - "telegram-bot-api": "bin/telegram-bot-api.js", - "threatbook-cloudapi-v3": "bin/threatbook-cloudapi-v3.js", "threatbook-claudsandbox-v3": "bin/threatbook-claudsandbox-v3.js", + "threatbook-cloudapi-v3": "bin/threatbook-cloudapi-v3.js", "threatbook-ngtip-v5": "bin/threatbook-ngtip-v5.js", "threatbook-onesig": "bin/threatbook-onesig.js", "threatbook-tdp": "bin/threatbook-tdp.js", "threatbook-tip-v4": "bin/threatbook-tip-v4.js", + "topsec-fw-2u": "bin/topsec-fw-2u.js", "topsec-fw-5u": "bin/topsec-fw-5u.js", "topsec-fw-v3-7-6": "bin/topsec-fw-v3-7-6.js", - "topsec-fw-2u": "bin/topsec-fw-2u.js", "topsec-waf-v3-2294-20238": "bin/topsec-waf-v3-2294-20238.js", "venus-ads-v3-6": "bin/venus-ads-v3-6.js", "volcengine-cloud-firewall": "bin/volcengine-cloud-firewall.js", "wangsu-label-ip": "bin/wangsu-label-ip.js", - "wd-k01": "bin/wd-k01.js", - "tencent-bh": "bin/tencent-bh.js", - "alibaba-sas": "bin/alibaba-sas.js", - "misp": "bin/misp.js", - "opencti": "bin/opencti.js" + "wd-k01": "bin/wd-k01.js" }, "files": [ "bin/alibaba-cloud-simple-application-server-firewall.js", @@ -79,20 +81,24 @@ "bin/dptech-eds.js", "bin/dptech-fw-v4-6-10.js", "bin/dptech-umc-ads-v5-3-29.js", + "bin/dsensor.js", "bin/elastic-kibana-7-17-26.js", "bin/feishu-group-robot.js", "bin/first-epss-v1.js", + "bin/fofa-network-space-mapper.js", "bin/fortinet-fw.js", "bin/fortinet-waf.js", "bin/hermes-gateway.js", + "bin/hillstone-fw-v5-5-r10.js", "bin/hillstone-fw-v5-5-r4.js", "bin/hillstone-fw-v5-5-r6.js", - "bin/hillstone-fw-v5-5-r10.js", - "bin/huorong-endpoint-security-management-system-v2-0-19-3.js", "bin/huawei-fw-usg6000e.js", + "bin/huorong-endpoint-security-management-system-v2-0-19-3.js", "bin/imperva-waf-gateway-v13-6-90.js", "bin/nsfocus-ads-v4-5-r90-f06.js", + "bin/nsfocus-ngfw-v60-9900.js", "bin/nsfocus-nips-v5-6-r11.js", + "bin/opencti.js", "bin/owasp-dependency-track-sca-v5-0.js", "bin/panabit-tang-r1.js", "bin/qianxin-fw-secgate3600.js", @@ -103,18 +109,20 @@ "bin/qingteng-hids-v5.js", "bin/ray-waf-v6-1-2.js", "bin/riversafe-waf.js", - "bin/skycloud-inet.js", - "bin/slack-group-robot.js", - "bin/safeline-waf-eliminate-false-positive.js", + "bin/ruijie-behavior-firewall-r2-3-2-t0.js", "bin/safeline-waf.js", + "bin/safeline-waf-eliminate-false-positive.js", "bin/sangfor-fw-v8-0-45.js", "bin/sangfor-xdr-v2-0-45.js", + "bin/skycloud-inet.js", + "bin/slack-group-robot.js", + "bin/telegram-bot-api.js", + "bin/tencent-qq-chat.js", "bin/tencent-qyweixin-group-robot.js", "bin/tencent-tix-saas.js", "bin/tencent-tsec-v2-5-1.js", - "bin/telegram-bot-api.js", - "bin/threatbook-cloudapi-v3.js", "bin/threatbook-claudsandbox-v3.js", + "bin/threatbook-cloudapi-v3.js", "bin/threatbook-ngtip-v5.js", "bin/threatbook-onesig.js", "bin/threatbook-tdp.js", @@ -125,12 +133,8 @@ "bin/topsec-waf-v3-2294-20238.js", "bin/venus-ads-v3-6.js", "bin/volcengine-cloud-firewall.js", - "bin/wd-k01.js", - "bin/tencent-bh.js", - "bin/alibaba-sas.js", - "bin/misp.js", - "bin/opencti.js", "bin/wangsu-label-ip.js", + "bin/wd-k01.js", "alibaba-cloud__simple-application-server-firewall", "chaitin__cloudatlas", "das__gateway_v3", @@ -141,20 +145,24 @@ "dptech__eds", "dptech__fw_v4-6-10", "dptech__umc-ads_v5-3-29", + "chaitin__dsensor_ds-s_h_40-25.07.001", "elastic__kibana_7-17-26", "feishu__group-robot", "first__epss-v1", + "fofa__network-space-mapper", "fortinet__fw", "fortinet__waf", "hermes__gateway", + "hillstone__fw_v5-5-r10", "hillstone__fw_v5-5-r4", "hillstone__fw_v5-5-r6", - "hillstone__fw_v5-5-r10", - "huorong__endpoint-security-management-system_v2-0-19-3", "huawei__fw-usg6000e", + "huorong__endpoint-security-management-system_v2-0-19-3", "imperva__waf-gateway_v13-6-90", "nsfocus__ads_v4-5-r90-f06", + "nsfocus__ngfw_v60-9900", "nsfocus__nips_v5-6-r11", + "filigran__opencti", "owasp__dependency-track-sca_v5-0", "panabit__tang-r1", "qianxin__fw-secgate3600", @@ -165,18 +173,20 @@ "qingteng__hids_v5", "ray__waf_v6-1-2", "riversafe__waf", - "skycloud__inet", - "slack__group-robot", - "chaitin__safeline-waf-eliminate-false-positive", + "ruijie__behavior_firewall_r2-3-2-t0", "chaitin__safeline-waf", + "chaitin__safeline-waf-eliminate-false-positive", "sangfor__fw_v8-0-45", "sangfor__xdr_v2-0-45", + "skycloud__inet", + "slack__group-robot", + "telegram__bot-api", + "tencent__qq-chat", "tencent__qyweixin-group-robot", "tencent__tix-saas", "tencent__tsec_v2-5-1", - "telegram__bot-api", - "threatbook__cloudapi_v3", "threatbook__claudsandbox_v3", + "threatbook__cloudapi_v3", "threatbook__ngtip_v5", "threatbook__onesig", "threatbook__tdp", @@ -187,16 +197,14 @@ "topsec__waf_v3-2294-20238", "venus__ads_v3-6", "volcengine__cloud-firewall", - "wd__k01", - "tencent__bh", - "alibaba__sas", - "misp__misp", - "filigran__opencti", "wangsu__label-ip", + "wd__k01", "scripts", "bin/octobus-tentacles.js" ], "scripts": { + "registry:generate": "node scripts/generate-service-registry.mjs", + "registry:check": "node scripts/generate-service-registry.mjs --check", "validate": "node scripts/validate-service-package.mjs", "test": "node scripts/run-tests.mjs", "coverage:all": "node scripts/run-coverage-all.mjs", diff --git a/services/scripts/generate-service-registry.mjs b/services/scripts/generate-service-registry.mjs new file mode 100644 index 00000000..84bee492 --- /dev/null +++ b/services/scripts/generate-service-registry.mjs @@ -0,0 +1,410 @@ +#!/usr/bin/env node +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const ROOT_BIN_NAME = "octobus-tentacles"; +const ROOT_BIN_TARGET = "bin/octobus-tentacles.js"; +const SERVICE_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; +const EXECUTABLE_MODE = 0o755; + +function readJSON(filePath) { + try { + return JSON.parse(fs.readFileSync(filePath, "utf8")); + } catch (error) { + throw new Error(`${filePath}: failed to read JSON: ${error.message}`); + } +} + +function writeJSON(filePath, value) { + fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`); +} + +function filepathFromPackagePath(packagePath) { + return packagePath.split("/").join(path.sep); +} + +function parseArgs(argv) { + const opts = { + check: false, + root: path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."), + }; + + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i]; + if (arg === "--check") { + opts.check = true; + continue; + } + if (arg === "--root") { + if (i + 1 >= argv.length || argv[i + 1] === "") { + throw new Error("--root must not be empty"); + } + opts.root = argv[++i]; + continue; + } + if (arg.startsWith("--root=")) { + opts.root = arg.slice("--root=".length); + continue; + } + throw new Error(`unknown argument: ${arg}`); + } + + if (opts.root === "") { + throw new Error("--root must not be empty"); + } + + return { + ...opts, + root: path.resolve(opts.root), + }; +} + +function isServiceDir(root, entry) { + if (!entry.isDirectory()) { + return false; + } + if (entry.name === "bin" || entry.name === "scripts" || entry.name === "tests" || entry.name === "node_modules") { + return false; + } + if (entry.name.startsWith(".")) { + return false; + } + return fs.existsSync(path.join(root, entry.name, "service.json")); +} + +export function discoverServices(root) { + const services = fs.readdirSync(root, { withFileTypes: true }) + .filter((entry) => isServiceDir(root, entry)) + .map((entry) => { + const dir = entry.name; + const manifest = readJSON(path.join(root, dir, "service.json")); + if (!SERVICE_NAME_RE.test(manifest.name ?? "")) { + throw new Error(`${dir}/service.json name "${manifest.name ?? ""}" must match ${SERVICE_NAME_RE}`); + } + return { + dir, + name: manifest.name, + rootBinTarget: `bin/${manifest.name}.js`, + serviceEntry: `${dir}/bin/${manifest.name}.js`, + serviceModule: `${dir}/src/service.js`, + }; + }) + .sort((a, b) => a.name.localeCompare(b.name)); + + const seen = new Map(); + for (const service of services) { + const previous = seen.get(service.name); + if (previous != null) { + throw new Error(`duplicate service name "${service.name}" in ${previous} and ${service.dir}`); + } + seen.set(service.name, service.dir); + } + + return services; +} + +function assertServiceFiles(root, services) { + const errors = []; + for (const service of services) { + for (const [label, relativePath] of [ + ["service entry", service.serviceEntry], + ["service module", service.serviceModule], + ]) { + const fullPath = path.join(root, filepathFromPackagePath(relativePath)); + let stat; + try { + stat = fs.statSync(fullPath); + } catch { + errors.push(`${service.dir} ${label} "${relativePath}" must exist`); + continue; + } + if (!stat.isFile()) { + errors.push(`${service.dir} ${label} "${relativePath}" must be a file`); + continue; + } + if (label === "service entry" && (stat.mode & 0o111) === 0) { + errors.push(`${service.dir} ${label} "${relativePath}" must be executable`); + } + } + } + return errors; +} + +function makeServiceEntriesExecutable(root, services) { + for (const service of services) { + const entryPath = path.join(root, filepathFromPackagePath(service.serviceEntry)); + if (!fs.existsSync(entryPath)) { + continue; + } + const stat = fs.statSync(entryPath); + if (stat.isFile() && (stat.mode & 0o111) === 0) { + fs.chmodSync(entryPath, EXECUTABLE_MODE); + } + } +} + +function desiredPackage(pkg, services) { + const next = { + ...pkg, + bin: { + [ROOT_BIN_NAME]: ROOT_BIN_TARGET, + }, + }; + + for (const service of services) { + next.bin[service.name] = service.rootBinTarget; + } + + next.files = [ + ...services.map((service) => service.rootBinTarget), + ...services.map((service) => service.dir), + "scripts", + ROOT_BIN_TARGET, + ]; + + return next; +} + +function desiredDispatcher(services) { + const serviceMap = services.map((service) => ` "${service.name}": { + entryFile: "../${service.serviceEntry}", + serviceModule: "../${service.serviceModule}", + },`).join("\n"); + + return `#!/usr/bin/env node + +import { fileURLToPath } from "node:url"; +import { runServiceMain } from "@chaitin-ai/octobus-sdk"; +import { Command } from "commander"; + +const services = { +${serviceMap} +}; + +const serviceNames = Object.keys(services); + +const program = new Command(); + +program + .name("octobus-tentacles") + .usage(" [args]") + .description("Run a service from this package") + .argument("[service]", "service name") + .allowUnknownOption(true) + .allowExcessArguments(true) + .passThroughOptions() + .addHelpText("after", \` +Services: +\${serviceNames.map((name) => \` \${name.padEnd(37)}\`).join("\\n")} + +Use 'octobus-tentacles --help' to print service help.\`) + .action(async (serviceName) => { + if (!serviceName) { + program.outputHelp(); + return; + } + + const selected = services[serviceName]; + if (!selected) { + process.stderr.write(\`Unknown service: \${serviceName}\\n\\n\`); + program.outputHelp({ error: true }); + process.exitCode = 1; + return; + } + + const { service } = await import(new URL(selected.serviceModule, import.meta.url)); + + await runServiceMain(service, { + argv: program.args.slice(1), + entryFile: fileURLToPath(new URL(selected.entryFile, import.meta.url)), + }); + }); + +await program.parseAsync(); +`; +} + +function desiredWrapper(service) { + return `#!/usr/bin/env node + +import { fileURLToPath } from "node:url"; +import { runServiceMain } from "@chaitin-ai/octobus-sdk"; + +import { service } from "../${service.serviceModule}"; + +runServiceMain(service, { + entryFile: fileURLToPath(new URL("../${service.serviceEntry}", import.meta.url)), +}); +`; +} + +function isValidCustomWrapper(source, service) { + return source.includes("import { fileURLToPath } from \"node:url\";") + && source.includes("runServiceMain(service, {") + && source.includes(`../${service.serviceModule}`) + && source.includes(`entryFile: fileURLToPath(new URL("../${service.serviceEntry}", import.meta.url))`); +} + +function compareText(errors, label, actualPath, expected) { + if (!fs.existsSync(actualPath)) { + errors.push(`${label} must exist`); + return; + } + if (fs.readFileSync(actualPath, "utf8") !== expected) { + errors.push(`${label} is not up to date`); + } +} + +function compareJSON(errors, label, actualPath, expected) { + if (!fs.existsSync(actualPath)) { + errors.push(`${label} must exist`); + return; + } + const actual = readJSON(actualPath); + if (`${JSON.stringify(actual, null, 2)}\n` !== `${JSON.stringify(expected, null, 2)}\n`) { + errors.push(`${label} is not up to date`); + } +} + +function checkDuplicateFiles(errors, pkg) { + if (!Array.isArray(pkg.files)) { + errors.push("package.json files must be an array"); + return; + } + const seen = new Set(); + for (const entry of pkg.files) { + if (seen.has(entry)) { + errors.push(`package.json files contains duplicate entry "${entry}"`); + } + seen.add(entry); + } +} + +function checkExecutable(errors, filePath, label) { + if (!fs.existsSync(filePath)) { + errors.push(`${label} must exist`); + return; + } + const stat = fs.statSync(filePath); + if (!stat.isFile()) { + errors.push(`${label} must be a file`); + return; + } + if ((stat.mode & 0o111) === 0) { + errors.push(`${label} must be executable`); + } +} + +function checkWrappers(errors, root, services) { + for (const service of services) { + const wrapperPath = path.join(root, filepathFromPackagePath(service.rootBinTarget)); + const expected = desiredWrapper(service); + if (!fs.existsSync(wrapperPath)) { + errors.push(`${service.rootBinTarget} must exist`); + continue; + } + checkExecutable(errors, wrapperPath, service.rootBinTarget); + const actual = fs.readFileSync(wrapperPath, "utf8"); + if (actual !== expected && !isValidCustomWrapper(actual, service)) { + errors.push(`${service.rootBinTarget} must point to ${service.dir}`); + } + } +} + +function updateWrapper(root, service) { + const wrapperPath = path.join(root, filepathFromPackagePath(service.rootBinTarget)); + const expected = desiredWrapper(service); + fs.mkdirSync(path.dirname(wrapperPath), { recursive: true }); + if (fs.existsSync(wrapperPath)) { + const actual = fs.readFileSync(wrapperPath, "utf8"); + if (actual !== expected && isValidCustomWrapper(actual, service)) { + fs.chmodSync(wrapperPath, EXECUTABLE_MODE); + return false; + } + } + fs.writeFileSync(wrapperPath, expected); + fs.chmodSync(wrapperPath, EXECUTABLE_MODE); + return true; +} + +export function checkRegistry(root) { + const services = discoverServices(root); + const errors = assertServiceFiles(root, services); + const packagePath = path.join(root, "package.json"); + const dispatcherPath = path.join(root, filepathFromPackagePath(ROOT_BIN_TARGET)); + const pkg = fs.existsSync(packagePath) ? readJSON(packagePath) : {}; + + compareJSON(errors, "package.json", packagePath, desiredPackage(pkg, services)); + checkDuplicateFiles(errors, pkg); + compareText(errors, ROOT_BIN_TARGET, dispatcherPath, desiredDispatcher(services)); + checkExecutable(errors, dispatcherPath, ROOT_BIN_TARGET); + checkWrappers(errors, root, services); + + return { errors }; +} + +export function generateRegistry(root) { + const services = discoverServices(root); + makeServiceEntriesExecutable(root, services); + const serviceFileErrors = assertServiceFiles(root, services); + if (serviceFileErrors.length > 0) { + throw new Error(serviceFileErrors.join("\n")); + } + + const packagePath = path.join(root, "package.json"); + if (!fs.existsSync(packagePath)) { + throw new Error(`${packagePath}: missing package.json`); + } + const pkg = readJSON(packagePath); + writeJSON(packagePath, desiredPackage(pkg, services)); + + const dispatcherPath = path.join(root, filepathFromPackagePath(ROOT_BIN_TARGET)); + fs.mkdirSync(path.dirname(dispatcherPath), { recursive: true }); + fs.writeFileSync(dispatcherPath, desiredDispatcher(services)); + fs.chmodSync(dispatcherPath, EXECUTABLE_MODE); + + let writtenWrappers = 0; + for (const service of services) { + if (updateWrapper(root, service)) { + writtenWrappers += 1; + } + } + + return { + services: services.length, + writtenWrappers, + }; +} + +export function main(argv = process.argv.slice(2)) { + const opts = parseArgs(argv); + if (opts.check) { + const { errors } = checkRegistry(opts.root); + if (errors.length > 0) { + for (const error of errors) { + console.error(`error: ${error}`); + } + return 1; + } + console.log("service registry is up to date"); + return 0; + } + + const result = generateRegistry(opts.root); + console.log(`generated service registry for ${result.services} services`); + if (result.writtenWrappers > 0) { + console.log(`updated ${result.writtenWrappers} root wrappers`); + } + return 0; +} + +const entrypoint = fileURLToPath(import.meta.url); +if (process.argv[1] != null && path.resolve(process.argv[1]) === entrypoint) { + try { + process.exitCode = main(); + } catch (error) { + console.error(`error: ${error.message}`); + process.exitCode = 1; + } +} diff --git a/services/tests/validate-service-package.test.mjs b/services/tests/validate-service-package.test.mjs index 82c1b825..e07f6a36 100644 --- a/services/tests/validate-service-package.test.mjs +++ b/services/tests/validate-service-package.test.mjs @@ -5,6 +5,7 @@ import path from "node:path"; import test from "node:test"; import { discoverServices, main as importCheckMain } from "../scripts/import-check-all.mjs"; +import { checkRegistry, main as registryMain } from "../scripts/generate-service-registry.mjs"; import { main as runCoverageAllMain } from "../scripts/run-coverage-all.mjs"; import { main as runTestsMain, buildNodeTestArgs } from "../scripts/run-tests.mjs"; import { main as validateMain, validateRepository } from "../scripts/validate-service-package.mjs"; @@ -112,6 +113,19 @@ test("validates a migrated external service package root", () => { assert.deepEqual(result.errors, []); }); +test("generates and checks the aggregate service registry", () => { + const root = fixture(); + assert.equal(registryMain(["--root", root]), 0); + assert.equal(registryMain(["--root", root, "--check"]), 0); + assert.deepEqual(validateRepository(root, { serviceDir: "chaitin__safeline-waf" }).errors, []); + + fs.chmodSync(path.join(root, "bin", "safeline-waf.js"), 0o644); + assert.match( + checkRegistry(root).errors.join("\n"), + /bin\/safeline-waf\.js must be executable/, + ); +}); + test("requires executable root wrappers and service entries", () => { const root = fixture(); fs.chmodSync(path.join(root, "bin", "safeline-waf.js"), 0o644);