codeksei 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE +661 -0
  2. package/README.en.md +215 -0
  3. package/README.md +259 -0
  4. package/bin/codeksei.js +10 -0
  5. package/bin/cyberboss.js +11 -0
  6. package/package.json +86 -0
  7. package/scripts/install-background-tasks.ps1 +135 -0
  8. package/scripts/open_shared_wechat_thread.sh +94 -0
  9. package/scripts/open_wechat_thread.sh +117 -0
  10. package/scripts/shared-common.js +791 -0
  11. package/scripts/shared-open.js +46 -0
  12. package/scripts/shared-start.js +41 -0
  13. package/scripts/shared-status.js +74 -0
  14. package/scripts/shared-supervisor.js +141 -0
  15. package/scripts/shared-task-runner.ps1 +87 -0
  16. package/scripts/shared-watchdog.js +290 -0
  17. package/scripts/show_shared_status.sh +53 -0
  18. package/scripts/start_shared_app_server.sh +65 -0
  19. package/scripts/start_shared_wechat.sh +108 -0
  20. package/scripts/timeline-screenshot.sh +15 -0
  21. package/scripts/uninstall-background-tasks.ps1 +23 -0
  22. package/src/adapters/channel/weixin/account-store.js +135 -0
  23. package/src/adapters/channel/weixin/api-v2.js +258 -0
  24. package/src/adapters/channel/weixin/api.js +180 -0
  25. package/src/adapters/channel/weixin/context-token-store.js +84 -0
  26. package/src/adapters/channel/weixin/index.js +605 -0
  27. package/src/adapters/channel/weixin/legacy.js +567 -0
  28. package/src/adapters/channel/weixin/login-common.js +63 -0
  29. package/src/adapters/channel/weixin/login-legacy.js +124 -0
  30. package/src/adapters/channel/weixin/login-v2.js +186 -0
  31. package/src/adapters/channel/weixin/media-mime.js +22 -0
  32. package/src/adapters/channel/weixin/media-receive.js +370 -0
  33. package/src/adapters/channel/weixin/media-send.js +331 -0
  34. package/src/adapters/channel/weixin/message-utils-v2.js +282 -0
  35. package/src/adapters/channel/weixin/message-utils.js +199 -0
  36. package/src/adapters/channel/weixin/protocol.js +77 -0
  37. package/src/adapters/channel/weixin/redact.js +41 -0
  38. package/src/adapters/channel/weixin/reminder-queue-store.js +101 -0
  39. package/src/adapters/channel/weixin/sync-buffer-store.js +35 -0
  40. package/src/adapters/runtime/codex/events.js +252 -0
  41. package/src/adapters/runtime/codex/index.js +502 -0
  42. package/src/adapters/runtime/codex/message-utils.js +141 -0
  43. package/src/adapters/runtime/codex/model-catalog.js +106 -0
  44. package/src/adapters/runtime/codex/protocol-leak-monitor.js +75 -0
  45. package/src/adapters/runtime/codex/rpc-client.js +443 -0
  46. package/src/adapters/runtime/codex/session-store.js +376 -0
  47. package/src/app/channel-send-file-cli.js +57 -0
  48. package/src/app/diary-write-cli.js +620 -0
  49. package/src/app/note-auto-cli.js +201 -0
  50. package/src/app/note-sync-cli.js +130 -0
  51. package/src/app/project-radar-cli.js +165 -0
  52. package/src/app/reminder-write-cli.js +210 -0
  53. package/src/app/review-cli.js +134 -0
  54. package/src/app/system-checkin-poller.js +100 -0
  55. package/src/app/system-send-cli.js +129 -0
  56. package/src/app/timeline-event-cli.js +273 -0
  57. package/src/app/timeline-screenshot-cli.js +109 -0
  58. package/src/core/app.js +1810 -0
  59. package/src/core/branding.js +167 -0
  60. package/src/core/command-registry.js +609 -0
  61. package/src/core/config.js +84 -0
  62. package/src/core/default-targets.js +163 -0
  63. package/src/core/durable-note-schema.js +325 -0
  64. package/src/core/instructions-template.js +31 -0
  65. package/src/core/note-sync.js +433 -0
  66. package/src/core/project-radar.js +402 -0
  67. package/src/core/review-semantic.js +524 -0
  68. package/src/core/review.js +1081 -0
  69. package/src/core/shared-bridge-heartbeat.js +140 -0
  70. package/src/core/stream-delivery.js +990 -0
  71. package/src/core/system-message-dispatcher.js +68 -0
  72. package/src/core/system-message-queue-store.js +128 -0
  73. package/src/core/thread-state-store.js +135 -0
  74. package/src/core/timeline-screenshot-queue-store.js +134 -0
  75. package/src/core/workspace-alias.js +163 -0
  76. package/src/core/workspace-bootstrap.js +338 -0
  77. package/src/index.js +270 -0
  78. package/src/integrations/timeline/index.js +191 -0
  79. package/templates/weixin-instructions.md +53 -0
  80. package/templates/weixin-operations.md +69 -0
@@ -0,0 +1,53 @@
1
+ #!/bin/zsh
2
+ set -euo pipefail
3
+
4
+ PORT="${CODEKSEI_SHARED_PORT:-${CYBERBOSS_SHARED_PORT:-8765}}"
5
+ LISTEN_URL="ws://127.0.0.1:${PORT}"
6
+ STATE_DIR="${CODEKSEI_STATE_DIR:-${CYBERBOSS_STATE_DIR:-$HOME/.codeksei}}"
7
+ if [[ ! -d "${STATE_DIR}" && -d "$HOME/.cyberboss" ]]; then
8
+ STATE_DIR="$HOME/.cyberboss"
9
+ fi
10
+ LOG_DIR="${STATE_DIR}/logs"
11
+ APP_SERVER_PID_FILE="${LOG_DIR}/shared-app-server.pid"
12
+ WECHAT_PID_FILE="${LOG_DIR}/shared-wechat.pid"
13
+ WECHAT_LOG_FILE="${LOG_DIR}/shared-wechat.log"
14
+
15
+ function print_pid_state() {
16
+ local label="$1"
17
+ local pid_file="$2"
18
+
19
+ if [[ -f "${pid_file}" ]]; then
20
+ local pid
21
+ pid="$(cat "${pid_file}" 2>/dev/null || true)"
22
+ if [[ -n "${pid}" ]] && kill -0 "${pid}" 2>/dev/null; then
23
+ echo "${label}=${pid}"
24
+ return
25
+ fi
26
+ echo "${label}=stale"
27
+ return
28
+ fi
29
+
30
+ echo "${label}=missing"
31
+ }
32
+
33
+ echo "listen=${LISTEN_URL}"
34
+ print_pid_state "shared_app_server_pid" "${APP_SERVER_PID_FILE}"
35
+ print_pid_state "shared_codeksei_pid" "${WECHAT_PID_FILE}"
36
+ print_pid_state "shared_cyberboss_pid" "${WECHAT_PID_FILE}"
37
+
38
+ if command -v curl >/dev/null 2>&1; then
39
+ if curl -sf "http://127.0.0.1:${PORT}/readyz" >/dev/null; then
40
+ echo "readyz=ok"
41
+ else
42
+ echo "readyz=down"
43
+ fi
44
+ fi
45
+
46
+ if command -v lsof >/dev/null 2>&1; then
47
+ lsof -nP -iTCP:"${PORT}" -sTCP:LISTEN || true
48
+ fi
49
+
50
+ if [[ -f "${WECHAT_LOG_FILE}" ]]; then
51
+ echo "--- ${WECHAT_LOG_FILE} (tail) ---"
52
+ tail -n 20 "${WECHAT_LOG_FILE}" || true
53
+ fi
@@ -0,0 +1,65 @@
1
+ #!/bin/zsh
2
+ set -euo pipefail
3
+
4
+ PORT="${CODEKSEI_SHARED_PORT:-${CYBERBOSS_SHARED_PORT:-8765}}"
5
+ LISTEN_URL="ws://127.0.0.1:${PORT}"
6
+ STATE_DIR="${CODEKSEI_STATE_DIR:-${CYBERBOSS_STATE_DIR:-$HOME/.codeksei}}"
7
+ if [[ ! -d "${STATE_DIR}" && -d "$HOME/.cyberboss" ]]; then
8
+ STATE_DIR="$HOME/.cyberboss"
9
+ fi
10
+ LOG_DIR="${STATE_DIR}/logs"
11
+ PID_FILE="${LOG_DIR}/shared-app-server.pid"
12
+ LOG_FILE="${LOG_DIR}/shared-app-server.log"
13
+
14
+ function lookup_listen_pid() {
15
+ lsof -nP -iTCP:"${PORT}" -sTCP:LISTEN 2>/dev/null \
16
+ | awk 'NR > 1 { print $2; found=1; exit } END { if (!found) exit 0 }'
17
+ }
18
+
19
+ mkdir -p "${LOG_DIR}"
20
+ export CODEKSEI_STATE_DIR="${STATE_DIR}"
21
+ export CYBERBOSS_STATE_DIR="${STATE_DIR}"
22
+ export TIMELINE_FOR_AGENT_STATE_DIR="${STATE_DIR}"
23
+ if [[ -z "${TIMELINE_FOR_AGENT_CHROME_PATH:-}" ]]; then
24
+ export TIMELINE_FOR_AGENT_CHROME_PATH="${CODEKSEI_SCREENSHOT_CHROME_PATH:-${CYBERBOSS_SCREENSHOT_CHROME_PATH:-/Applications/Google Chrome.app/Contents/MacOS/Google Chrome}}"
25
+ fi
26
+
27
+ if [[ -f "${PID_FILE}" ]]; then
28
+ EXISTING_PID="$(cat "${PID_FILE}")"
29
+ if [[ -n "${EXISTING_PID}" ]] && kill -0 "${EXISTING_PID}" 2>/dev/null; then
30
+ echo "shared app-server already running pid=${EXISTING_PID} listen=${LISTEN_URL}"
31
+ exit 0
32
+ fi
33
+ rm -f "${PID_FILE}"
34
+ fi
35
+
36
+ EXISTING_PID="$(lookup_listen_pid || true)"
37
+ if [[ -n "${EXISTING_PID}" ]]; then
38
+ echo "${EXISTING_PID}" > "${PID_FILE}"
39
+ echo "shared app-server already running pid=${EXISTING_PID} listen=${LISTEN_URL}"
40
+ exit 0
41
+ fi
42
+
43
+ nohup codex app-server --listen "${LISTEN_URL}" >> "${LOG_FILE}" 2>&1 &
44
+ APP_SERVER_PID=$!
45
+ echo "${APP_SERVER_PID}" > "${PID_FILE}"
46
+ sleep 1
47
+
48
+ LISTEN_PID="$(lookup_listen_pid || true)"
49
+ if kill -0 "${APP_SERVER_PID}" 2>/dev/null && [[ -n "${LISTEN_PID}" ]]; then
50
+ echo "${LISTEN_PID}" > "${PID_FILE}"
51
+ echo "started shared app-server pid=${LISTEN_PID} listen=${LISTEN_URL}"
52
+ echo "log=${LOG_FILE}"
53
+ exit 0
54
+ fi
55
+
56
+ EXISTING_PID="$(lookup_listen_pid || true)"
57
+ if [[ -n "${EXISTING_PID}" ]]; then
58
+ echo "${EXISTING_PID}" > "${PID_FILE}"
59
+ echo "shared app-server already running pid=${EXISTING_PID} listen=${LISTEN_URL}"
60
+ exit 0
61
+ fi
62
+
63
+ echo "failed to start shared app-server; check ${LOG_FILE}" >&2
64
+ tail -n 20 "${LOG_FILE}" >&2 || true
65
+ exit 1
@@ -0,0 +1,108 @@
1
+ #!/bin/zsh
2
+ set -euo pipefail
3
+
4
+ ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
5
+ PORT="${CODEKSEI_SHARED_PORT:-${CYBERBOSS_SHARED_PORT:-8765}}"
6
+ STATE_DIR="${CODEKSEI_STATE_DIR:-${CYBERBOSS_STATE_DIR:-$HOME/.codeksei}}"
7
+ if [[ ! -d "${STATE_DIR}" && -d "$HOME/.cyberboss" ]]; then
8
+ STATE_DIR="$HOME/.cyberboss"
9
+ fi
10
+ LOG_DIR="${STATE_DIR}/logs"
11
+ PID_FILE="${LOG_DIR}/shared-wechat.pid"
12
+
13
+ function resolve_pid_cwd() {
14
+ local pid="$1"
15
+ lsof -a -p "${pid}" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p' | head -n 1
16
+ }
17
+
18
+ function list_bridge_processes() {
19
+ ps -ax -o pid=,ppid=,command= | awk '/node \.\/bin\/(codeksei|cyberboss)\.js start --checkin/ { print }'
20
+ }
21
+
22
+ function find_bridge_child_pid() {
23
+ local parent_pid="$1"
24
+ list_bridge_processes | awk -v target_ppid="${parent_pid}" '$2 == target_ppid { print $1; exit }'
25
+ }
26
+
27
+ function resolve_bridge_pid() {
28
+ local candidate_pid="$1"
29
+ [[ -n "${candidate_pid}" ]] || return 1
30
+ if ! kill -0 "${candidate_pid}" 2>/dev/null; then
31
+ return 1
32
+ fi
33
+
34
+ local child_pid
35
+ child_pid="$(find_bridge_child_pid "${candidate_pid}")"
36
+ if [[ -n "${child_pid}" ]]; then
37
+ echo "${child_pid}"
38
+ return 0
39
+ fi
40
+
41
+ if [[ "$(resolve_pid_cwd "${candidate_pid}")" == "${ROOT_DIR}" ]]; then
42
+ echo "${candidate_pid}"
43
+ return 0
44
+ fi
45
+
46
+ return 1
47
+ }
48
+
49
+ function find_existing_bridge_pid() {
50
+ if [[ -f "${PID_FILE}" ]]; then
51
+ local pid_from_file
52
+ pid_from_file="$(cat "${PID_FILE}" 2>/dev/null || true)"
53
+ local resolved_from_file
54
+ resolved_from_file="$(resolve_bridge_pid "${pid_from_file}" || true)"
55
+ if [[ -n "${resolved_from_file}" ]]; then
56
+ echo "${resolved_from_file}"
57
+ return 0
58
+ fi
59
+ fi
60
+
61
+ local pid
62
+ while read -r pid _; do
63
+ [[ -n "${pid}" ]] || continue
64
+ if [[ "$(resolve_pid_cwd "${pid}")" == "${ROOT_DIR}" ]]; then
65
+ echo "${pid}"
66
+ return 0
67
+ fi
68
+ done < <(list_bridge_processes)
69
+
70
+ return 1
71
+ }
72
+
73
+ function cleanup_pid_file() {
74
+ if [[ -f "${PID_FILE}" ]]; then
75
+ local current_pid
76
+ current_pid="$(cat "${PID_FILE}" 2>/dev/null || true)"
77
+ if [[ "${current_pid}" == "$$" ]]; then
78
+ rm -f "${PID_FILE}"
79
+ fi
80
+ fi
81
+ }
82
+
83
+ "${ROOT_DIR}/scripts/start_shared_app_server.sh"
84
+ mkdir -p "${LOG_DIR}"
85
+
86
+ EXISTING_PID="$(find_existing_bridge_pid || true)"
87
+ if [[ -n "${EXISTING_PID}" ]]; then
88
+ echo "${EXISTING_PID}" > "${PID_FILE}"
89
+ echo "shared codeksei already running pid=${EXISTING_PID}"
90
+ exit 0
91
+ fi
92
+
93
+ BRIDGE_PID=""
94
+ function shutdown_bridge() {
95
+ if [[ -n "${BRIDGE_PID}" ]] && kill -0 "${BRIDGE_PID}" 2>/dev/null; then
96
+ kill "${BRIDGE_PID}" 2>/dev/null || true
97
+ fi
98
+ cleanup_pid_file
99
+ }
100
+
101
+ trap shutdown_bridge EXIT INT TERM
102
+ cd "${ROOT_DIR}"
103
+ export CODEKSEI_CODEX_ENDPOINT="ws://127.0.0.1:${PORT}"
104
+ export CYBERBOSS_CODEX_ENDPOINT="ws://127.0.0.1:${PORT}"
105
+ node ./bin/codeksei.js start --checkin &
106
+ BRIDGE_PID="$!"
107
+ echo "${BRIDGE_PID}" > "${PID_FILE}"
108
+ wait "${BRIDGE_PID}"
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
5
+ ARGS=()
6
+
7
+ for arg in "$@"; do
8
+ if [[ "$arg" == "--send" ]]; then
9
+ continue
10
+ fi
11
+ ARGS+=("$arg")
12
+ done
13
+
14
+ cd "$ROOT"
15
+ exec node ./bin/codeksei.js timeline screenshot "${ARGS[@]}"
@@ -0,0 +1,23 @@
1
+ param()
2
+
3
+ $ErrorActionPreference = "Stop"
4
+
5
+ $taskNames = @(
6
+ "Codeksei Shared Start",
7
+ "Codeksei Shared Unlock",
8
+ "Codeksei Shared Resume",
9
+ "Codeksei Shared Watchdog",
10
+ "Cyberboss Shared Start",
11
+ "Cyberboss Shared Unlock",
12
+ "Cyberboss Shared Resume",
13
+ "Cyberboss Shared Watchdog"
14
+ )
15
+
16
+ foreach ($taskName in $taskNames) {
17
+ try {
18
+ Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction Stop
19
+ Write-Host "removed=$taskName"
20
+ } catch {
21
+ Write-Host "missing=$taskName"
22
+ }
23
+ }
@@ -0,0 +1,135 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { normalizeRouteTag } = require("./protocol");
4
+
5
+ function normalizeAccountId(raw) {
6
+ return String(raw || "")
7
+ .trim()
8
+ .toLowerCase()
9
+ .replace(/[^a-z0-9._-]+/g, "-")
10
+ .replace(/-+/g, "-")
11
+ .replace(/^-|-$/g, "");
12
+ }
13
+
14
+ function ensureAccountsDir(config) {
15
+ fs.mkdirSync(config.accountsDir, { recursive: true });
16
+ }
17
+
18
+ function resolveAccountPath(config, accountId) {
19
+ return path.join(config.accountsDir, `${normalizeAccountId(accountId)}.json`);
20
+ }
21
+
22
+ function deleteWeixinAccount(config, accountId) {
23
+ const normalized = normalizeAccountId(accountId);
24
+ if (!normalized) {
25
+ return false;
26
+ }
27
+ try {
28
+ const filePath = resolveAccountPath(config, normalized);
29
+ if (!fs.existsSync(filePath)) {
30
+ return false;
31
+ }
32
+ fs.unlinkSync(filePath);
33
+ return true;
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+
39
+ function saveWeixinAccount(config, rawAccountId, update) {
40
+ ensureAccountsDir(config);
41
+ const accountId = normalizeAccountId(rawAccountId);
42
+ const filePath = resolveAccountPath(config, accountId);
43
+ const existing = loadWeixinAccount(config, accountId) || {};
44
+ const hasRouteTag = Object.prototype.hasOwnProperty.call(update || {}, "routeTag");
45
+ const next = {
46
+ accountId,
47
+ rawAccountId: String(rawAccountId || "").trim() || existing.rawAccountId || "",
48
+ token: typeof update.token === "string" && update.token.trim() ? update.token.trim() : existing.token || "",
49
+ baseUrl: typeof update.baseUrl === "string" && update.baseUrl.trim() ? update.baseUrl.trim() : existing.baseUrl || config.weixinBaseUrl,
50
+ userId: typeof update.userId === "string" ? update.userId.trim() : existing.userId || "",
51
+ routeTag: hasRouteTag
52
+ ? normalizeRouteTag(update.routeTag)
53
+ : normalizeRouteTag(existing.routeTag || config.weixinRouteTag),
54
+ savedAt: new Date().toISOString(),
55
+ };
56
+ fs.writeFileSync(filePath, JSON.stringify(next, null, 2), "utf8");
57
+ try {
58
+ fs.chmodSync(filePath, 0o600);
59
+ } catch {
60
+ // best effort
61
+ }
62
+ return next;
63
+ }
64
+
65
+ function loadWeixinAccount(config, accountId) {
66
+ const normalized = normalizeAccountId(accountId);
67
+ if (!normalized) {
68
+ return null;
69
+ }
70
+ try {
71
+ const raw = fs.readFileSync(resolveAccountPath(config, normalized), "utf8");
72
+ const parsed = JSON.parse(raw);
73
+ if (!parsed || typeof parsed !== "object") {
74
+ return null;
75
+ }
76
+ return {
77
+ accountId: normalized,
78
+ rawAccountId: typeof parsed.rawAccountId === "string" ? parsed.rawAccountId : "",
79
+ token: typeof parsed.token === "string" ? parsed.token : "",
80
+ baseUrl: typeof parsed.baseUrl === "string" && parsed.baseUrl.trim() ? parsed.baseUrl.trim() : config.weixinBaseUrl,
81
+ userId: typeof parsed.userId === "string" ? parsed.userId : "",
82
+ routeTag: Object.prototype.hasOwnProperty.call(parsed, "routeTag")
83
+ ? normalizeRouteTag(parsed.routeTag)
84
+ : normalizeRouteTag(config.weixinRouteTag),
85
+ savedAt: typeof parsed.savedAt === "string" ? parsed.savedAt : "",
86
+ };
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+
92
+ function listWeixinAccounts(config) {
93
+ ensureAccountsDir(config);
94
+ const files = fs.readdirSync(config.accountsDir, { withFileTypes: true });
95
+ return files
96
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json") && !entry.name.endsWith(".context-tokens.json"))
97
+ .map((entry) => loadWeixinAccount(config, entry.name.slice(0, -5)))
98
+ .filter(Boolean)
99
+ .sort((left, right) => String(right.savedAt || "").localeCompare(String(left.savedAt || "")));
100
+ }
101
+
102
+ function resolveSelectedAccount(config) {
103
+ if (config.accountId) {
104
+ const account = loadWeixinAccount(config, config.accountId);
105
+ if (!account) {
106
+ throw new Error(`未找到微信账号: ${config.accountId}`);
107
+ }
108
+ if (!account.token) {
109
+ throw new Error(`微信账号缺少 token: ${account.accountId},请重新执行 login`);
110
+ }
111
+ return account;
112
+ }
113
+ const accounts = listWeixinAccounts(config);
114
+ if (!accounts.length) {
115
+ throw new Error("当前没有已保存的微信账号,请先执行 `npm run login`");
116
+ }
117
+ if (accounts.length > 1) {
118
+ const accountIds = accounts.map((account) => account.accountId).join(", ");
119
+ throw new Error(`检测到多个微信账号,请设置 CODEKSEI_ACCOUNT_ID(或旧的 CYBERBOSS_ACCOUNT_ID)。可选值: ${accountIds}`);
120
+ }
121
+ if (!accounts[0].token) {
122
+ throw new Error(`微信账号缺少 token: ${accounts[0].accountId},请重新执行 login`);
123
+ }
124
+ return accounts[0];
125
+ }
126
+
127
+ module.exports = {
128
+ deleteWeixinAccount,
129
+ listWeixinAccounts,
130
+ loadWeixinAccount,
131
+ normalizeAccountId,
132
+ resolveAccountPath,
133
+ resolveSelectedAccount,
134
+ saveWeixinAccount,
135
+ };
@@ -0,0 +1,258 @@
1
+ const crypto = require("crypto");
2
+ const { buildJsonHeaders } = require("./protocol");
3
+ const { PRIMARY_CHANNEL_VERSION } = require("../../../core/branding");
4
+
5
+ const DEFAULT_LONG_POLL_TIMEOUT_MS = 35_000;
6
+ const DEFAULT_API_TIMEOUT_MS = 15_000;
7
+ const DEFAULT_CONFIG_TIMEOUT_MS = 10_000;
8
+ const MAX_RESPONSE_BODY_BYTES = 64 << 20;
9
+ const CHANNEL_VERSION = PRIMARY_CHANNEL_VERSION;
10
+
11
+ function buildBaseInfo() {
12
+ return { channel_version: CHANNEL_VERSION };
13
+ }
14
+
15
+ function ensureTrailingSlash(url) {
16
+ return url.endsWith("/") ? url : `${url}/`;
17
+ }
18
+
19
+ async function apiPost({
20
+ baseUrl,
21
+ endpoint,
22
+ token,
23
+ body,
24
+ timeoutMs = 0,
25
+ label,
26
+ routeTag = "",
27
+ clientVersion = "",
28
+ }) {
29
+ const url = new URL(endpoint, ensureTrailingSlash(baseUrl)).toString();
30
+ const controller = new AbortController();
31
+ const timeout = timeoutMs > 0 ? timeoutMs : DEFAULT_API_TIMEOUT_MS;
32
+ const timer = setTimeout(() => controller.abort(), timeout + 5_000);
33
+
34
+ try {
35
+ const response = await fetch(url, {
36
+ method: "POST",
37
+ headers: buildJsonHeaders({ body, token, routeTag, clientVersion }),
38
+ body,
39
+ signal: controller.signal,
40
+ });
41
+ const raw = await response.text();
42
+ if (Buffer.byteLength(raw, "utf8") > MAX_RESPONSE_BODY_BYTES) {
43
+ throw new Error(`${label} response body exceeds ${MAX_RESPONSE_BODY_BYTES} bytes`);
44
+ }
45
+ if (!response.ok) {
46
+ throw new Error(`${label} http ${response.status}: ${truncateForLog(raw, 512)}`);
47
+ }
48
+ return raw;
49
+ } finally {
50
+ clearTimeout(timer);
51
+ }
52
+ }
53
+
54
+ function parseJson(raw, label) {
55
+ try {
56
+ return JSON.parse(raw);
57
+ } catch (error) {
58
+ throw new Error(`${label} returned invalid JSON: ${truncateForLog(raw, 256)}`);
59
+ }
60
+ }
61
+
62
+ function assertApiSuccess(parsed, label) {
63
+ const ret = parsed?.ret;
64
+ const errcode = parsed?.errcode;
65
+ if ((ret !== undefined && ret !== 0) || (errcode !== undefined && errcode !== 0)) {
66
+ throw new Error(`${label} ret=${ret ?? ""} errcode=${errcode ?? ""} errmsg=${parsed?.errmsg ?? ""}`);
67
+ }
68
+ return parsed;
69
+ }
70
+
71
+ function truncateForLog(value, max) {
72
+ const text = typeof value === "string" ? value : String(value || "");
73
+ return text.length <= max ? text : `${text.slice(0, max)}…`;
74
+ }
75
+
76
+ async function getUpdatesV2({
77
+ baseUrl,
78
+ token,
79
+ getUpdatesBuf = "",
80
+ timeoutMs = DEFAULT_LONG_POLL_TIMEOUT_MS,
81
+ routeTag = "",
82
+ clientVersion = "",
83
+ }) {
84
+ const payload = JSON.stringify({
85
+ get_updates_buf: getUpdatesBuf,
86
+ base_info: buildBaseInfo(),
87
+ });
88
+ try {
89
+ const raw = await apiPost({
90
+ baseUrl,
91
+ endpoint: "ilink/bot/getupdates",
92
+ token,
93
+ body: payload,
94
+ timeoutMs,
95
+ label: "getUpdates",
96
+ routeTag,
97
+ clientVersion,
98
+ });
99
+ return parseJson(raw, "getUpdates");
100
+ } catch (error) {
101
+ if (error instanceof Error && error.name === "AbortError") {
102
+ return { ret: 0, msgs: [], get_updates_buf: getUpdatesBuf };
103
+ }
104
+ if (String(error?.message || "").includes("aborted")) {
105
+ return { ret: 0, msgs: [], get_updates_buf: getUpdatesBuf };
106
+ }
107
+ throw error;
108
+ }
109
+ }
110
+
111
+ async function sendTextV2({
112
+ baseUrl,
113
+ token,
114
+ toUserId,
115
+ text,
116
+ contextToken,
117
+ clientId,
118
+ routeTag = "",
119
+ clientVersion = "",
120
+ }) {
121
+ if (!String(contextToken || "").trim()) {
122
+ throw new Error("weixin-v2 sendText requires contextToken");
123
+ }
124
+ const itemList = [];
125
+ if (String(text || "").trim()) {
126
+ itemList.push({
127
+ type: 1,
128
+ text_item: { text: String(text) },
129
+ });
130
+ }
131
+ if (!itemList.length) {
132
+ throw new Error("weixin-v2 sendText requires non-empty text");
133
+ }
134
+ return sendMessageV2({
135
+ baseUrl,
136
+ token,
137
+ body: {
138
+ msg: {
139
+ from_user_id: "",
140
+ to_user_id: toUserId,
141
+ client_id: clientId || `cb-${crypto.randomUUID()}`,
142
+ message_type: 2,
143
+ message_state: 2,
144
+ item_list: itemList,
145
+ context_token: contextToken,
146
+ },
147
+ },
148
+ routeTag,
149
+ clientVersion,
150
+ });
151
+ }
152
+
153
+ async function sendMessageV2({
154
+ baseUrl,
155
+ token,
156
+ body,
157
+ routeTag = "",
158
+ clientVersion = "",
159
+ timeoutMs = DEFAULT_API_TIMEOUT_MS,
160
+ }) {
161
+ const raw = await apiPost({
162
+ baseUrl,
163
+ endpoint: "ilink/bot/sendmessage",
164
+ token,
165
+ body: JSON.stringify({
166
+ ...body,
167
+ base_info: buildBaseInfo(),
168
+ }),
169
+ timeoutMs,
170
+ label: "sendMessage",
171
+ routeTag,
172
+ clientVersion,
173
+ });
174
+ return assertApiSuccess(parseJson(raw, "sendMessage"), "sendMessage");
175
+ }
176
+
177
+ async function getConfigV2({
178
+ baseUrl,
179
+ token,
180
+ ilinkUserId,
181
+ contextToken,
182
+ routeTag = "",
183
+ clientVersion = "",
184
+ timeoutMs = DEFAULT_CONFIG_TIMEOUT_MS,
185
+ }) {
186
+ const raw = await apiPost({
187
+ baseUrl,
188
+ endpoint: "ilink/bot/getconfig",
189
+ token,
190
+ body: JSON.stringify({
191
+ ilink_user_id: ilinkUserId,
192
+ context_token: contextToken,
193
+ base_info: buildBaseInfo(),
194
+ }),
195
+ timeoutMs,
196
+ label: "getConfig",
197
+ routeTag,
198
+ clientVersion,
199
+ });
200
+ return assertApiSuccess(parseJson(raw, "getConfig"), "getConfig");
201
+ }
202
+
203
+ async function sendTypingV2({
204
+ baseUrl,
205
+ token,
206
+ body,
207
+ routeTag = "",
208
+ clientVersion = "",
209
+ timeoutMs = DEFAULT_CONFIG_TIMEOUT_MS,
210
+ }) {
211
+ const raw = await apiPost({
212
+ baseUrl,
213
+ endpoint: "ilink/bot/sendtyping",
214
+ token,
215
+ body: JSON.stringify({
216
+ ...body,
217
+ base_info: buildBaseInfo(),
218
+ }),
219
+ timeoutMs,
220
+ label: "sendTyping",
221
+ routeTag,
222
+ clientVersion,
223
+ });
224
+ return assertApiSuccess(parseJson(raw, "sendTyping"), "sendTyping");
225
+ }
226
+
227
+ async function getUploadUrlV2({
228
+ baseUrl,
229
+ token,
230
+ routeTag = "",
231
+ clientVersion = "",
232
+ timeoutMs = DEFAULT_API_TIMEOUT_MS,
233
+ ...payload
234
+ }) {
235
+ const raw = await apiPost({
236
+ baseUrl,
237
+ endpoint: "ilink/bot/getuploadurl",
238
+ token,
239
+ body: JSON.stringify({
240
+ ...payload,
241
+ base_info: buildBaseInfo(),
242
+ }),
243
+ timeoutMs,
244
+ label: "getUploadUrl",
245
+ routeTag,
246
+ clientVersion,
247
+ });
248
+ return assertApiSuccess(parseJson(raw, "getUploadUrl"), "getUploadUrl");
249
+ }
250
+
251
+ module.exports = {
252
+ getConfigV2,
253
+ getUpdatesV2,
254
+ getUploadUrlV2,
255
+ sendMessageV2,
256
+ sendTypingV2,
257
+ sendTextV2,
258
+ };