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.
- package/LICENSE +661 -0
- package/README.en.md +215 -0
- package/README.md +259 -0
- package/bin/codeksei.js +10 -0
- package/bin/cyberboss.js +11 -0
- package/package.json +86 -0
- package/scripts/install-background-tasks.ps1 +135 -0
- package/scripts/open_shared_wechat_thread.sh +94 -0
- package/scripts/open_wechat_thread.sh +117 -0
- package/scripts/shared-common.js +791 -0
- package/scripts/shared-open.js +46 -0
- package/scripts/shared-start.js +41 -0
- package/scripts/shared-status.js +74 -0
- package/scripts/shared-supervisor.js +141 -0
- package/scripts/shared-task-runner.ps1 +87 -0
- package/scripts/shared-watchdog.js +290 -0
- package/scripts/show_shared_status.sh +53 -0
- package/scripts/start_shared_app_server.sh +65 -0
- package/scripts/start_shared_wechat.sh +108 -0
- package/scripts/timeline-screenshot.sh +15 -0
- package/scripts/uninstall-background-tasks.ps1 +23 -0
- package/src/adapters/channel/weixin/account-store.js +135 -0
- package/src/adapters/channel/weixin/api-v2.js +258 -0
- package/src/adapters/channel/weixin/api.js +180 -0
- package/src/adapters/channel/weixin/context-token-store.js +84 -0
- package/src/adapters/channel/weixin/index.js +605 -0
- package/src/adapters/channel/weixin/legacy.js +567 -0
- package/src/adapters/channel/weixin/login-common.js +63 -0
- package/src/adapters/channel/weixin/login-legacy.js +124 -0
- package/src/adapters/channel/weixin/login-v2.js +186 -0
- package/src/adapters/channel/weixin/media-mime.js +22 -0
- package/src/adapters/channel/weixin/media-receive.js +370 -0
- package/src/adapters/channel/weixin/media-send.js +331 -0
- package/src/adapters/channel/weixin/message-utils-v2.js +282 -0
- package/src/adapters/channel/weixin/message-utils.js +199 -0
- package/src/adapters/channel/weixin/protocol.js +77 -0
- package/src/adapters/channel/weixin/redact.js +41 -0
- package/src/adapters/channel/weixin/reminder-queue-store.js +101 -0
- package/src/adapters/channel/weixin/sync-buffer-store.js +35 -0
- package/src/adapters/runtime/codex/events.js +252 -0
- package/src/adapters/runtime/codex/index.js +502 -0
- package/src/adapters/runtime/codex/message-utils.js +141 -0
- package/src/adapters/runtime/codex/model-catalog.js +106 -0
- package/src/adapters/runtime/codex/protocol-leak-monitor.js +75 -0
- package/src/adapters/runtime/codex/rpc-client.js +443 -0
- package/src/adapters/runtime/codex/session-store.js +376 -0
- package/src/app/channel-send-file-cli.js +57 -0
- package/src/app/diary-write-cli.js +620 -0
- package/src/app/note-auto-cli.js +201 -0
- package/src/app/note-sync-cli.js +130 -0
- package/src/app/project-radar-cli.js +165 -0
- package/src/app/reminder-write-cli.js +210 -0
- package/src/app/review-cli.js +134 -0
- package/src/app/system-checkin-poller.js +100 -0
- package/src/app/system-send-cli.js +129 -0
- package/src/app/timeline-event-cli.js +273 -0
- package/src/app/timeline-screenshot-cli.js +109 -0
- package/src/core/app.js +1810 -0
- package/src/core/branding.js +167 -0
- package/src/core/command-registry.js +609 -0
- package/src/core/config.js +84 -0
- package/src/core/default-targets.js +163 -0
- package/src/core/durable-note-schema.js +325 -0
- package/src/core/instructions-template.js +31 -0
- package/src/core/note-sync.js +433 -0
- package/src/core/project-radar.js +402 -0
- package/src/core/review-semantic.js +524 -0
- package/src/core/review.js +1081 -0
- package/src/core/shared-bridge-heartbeat.js +140 -0
- package/src/core/stream-delivery.js +990 -0
- package/src/core/system-message-dispatcher.js +68 -0
- package/src/core/system-message-queue-store.js +128 -0
- package/src/core/thread-state-store.js +135 -0
- package/src/core/timeline-screenshot-queue-store.js +134 -0
- package/src/core/workspace-alias.js +163 -0
- package/src/core/workspace-bootstrap.js +338 -0
- package/src/index.js +270 -0
- package/src/integrations/timeline/index.js +191 -0
- package/templates/weixin-instructions.md +53 -0
- 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
|
+
};
|