chore: sync local project state
This commit is contained in:
116
scripts/gateway_closure_inspect.sh
Normal file
116
scripts/gateway_closure_inspect.sh
Normal file
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="${BASE_URL:-http://127.0.0.1:8080}"
|
||||
CONSUMER="${CONSUMER:-gateway}"
|
||||
APPLIED_RATIO_THRESHOLD="${APPLIED_RATIO_THRESHOLD:-0.95}"
|
||||
FAILED_BURST_THRESHOLD="${FAILED_BURST_THRESHOLD:-3}"
|
||||
PENDING_RETRY_THRESHOLD="${PENDING_RETRY_THRESHOLD:-10}"
|
||||
|
||||
need() {
|
||||
command -v "$1" >/dev/null 2>&1 || {
|
||||
echo "missing required command: $1" >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
need curl
|
||||
need python3
|
||||
|
||||
health=$(curl -fsS "$BASE_URL/healthz")
|
||||
metrics=$(curl -fsS "$BASE_URL/metrics")
|
||||
status=$(curl -fsS "$BASE_URL/internal/supply-intelligence/gateway/runtime-status")
|
||||
|
||||
echo "=== healthz ==="
|
||||
echo "$health"
|
||||
echo "=== runtime status ==="
|
||||
echo "$status"
|
||||
echo "=== metrics excerpt ==="
|
||||
printf '%s
|
||||
' "$metrics" | grep 'supply_intelligence_gateway_' || true
|
||||
|
||||
export METRICS_TEXT="$metrics"
|
||||
export RUNTIME_STATUS_JSON="$status"
|
||||
export CONSUMER
|
||||
export APPLIED_RATIO_THRESHOLD
|
||||
export FAILED_BURST_THRESHOLD
|
||||
export PENDING_RETRY_THRESHOLD
|
||||
|
||||
python3 <<'PY'
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
metrics = os.environ['METRICS_TEXT']
|
||||
status = json.loads(os.environ['RUNTIME_STATUS_JSON'])
|
||||
consumer = os.environ['CONSUMER']
|
||||
ratio_threshold = float(os.environ['APPLIED_RATIO_THRESHOLD'])
|
||||
failed_threshold = int(os.environ['FAILED_BURST_THRESHOLD'])
|
||||
pending_threshold = int(os.environ['PENDING_RETRY_THRESHOLD'])
|
||||
|
||||
processed = {}
|
||||
for line in metrics.splitlines():
|
||||
if not line.startswith('supply_intelligence_gateway_events_processed_total'):
|
||||
continue
|
||||
head, _, tail = line.rpartition(' ')
|
||||
if not tail:
|
||||
continue
|
||||
m = re.search(r'\{([^}]*)\}$', head)
|
||||
if not m:
|
||||
continue
|
||||
labels = {}
|
||||
for part in m.group(1).split(','):
|
||||
if '=' not in part:
|
||||
continue
|
||||
k, v = part.split('=', 1)
|
||||
labels[k.strip()] = v.strip().strip('"')
|
||||
result_label = labels.get('result')
|
||||
if not result_label:
|
||||
continue
|
||||
processed[result_label] = processed.get(result_label, 0.0) + float(tail)
|
||||
|
||||
pending_retry = 0.0
|
||||
failed_events = 0.0
|
||||
for line in metrics.splitlines():
|
||||
if line.startswith('supply_intelligence_gateway_pending_retry_events') and f'consumer="{consumer}"' in line:
|
||||
pending_retry = float(line.rsplit(' ', 1)[-1])
|
||||
if line.startswith('supply_intelligence_gateway_failed_events') and f'consumer="{consumer}"' in line:
|
||||
failed_events = float(line.rsplit(' ', 1)[-1])
|
||||
|
||||
total_terminal = processed.get('applied', 0.0) + processed.get('failed', 0.0)
|
||||
applied_ratio = (processed.get('applied', 0.0) / total_terminal) if total_terminal > 0 else 1.0
|
||||
|
||||
decision = 'continue'
|
||||
reasons = []
|
||||
if not status.get('started', False):
|
||||
decision = 'pause'
|
||||
reasons.append('runtime_not_started')
|
||||
if status.get('last_error'):
|
||||
decision = 'pause'
|
||||
reasons.append('runtime_last_error')
|
||||
if pending_retry > pending_threshold:
|
||||
decision = 'pause'
|
||||
reasons.append('pending_retry_threshold_exceeded')
|
||||
if applied_ratio < ratio_threshold:
|
||||
decision = 'pause'
|
||||
reasons.append('applied_ratio_below_threshold')
|
||||
if failed_events >= failed_threshold:
|
||||
decision = 'rollback'
|
||||
reasons.append('failed_events_threshold_exceeded')
|
||||
|
||||
print(json.dumps({
|
||||
'decision': decision,
|
||||
'reasons': reasons,
|
||||
'applied_ratio': applied_ratio,
|
||||
'processed': processed,
|
||||
'pending_retry_events': pending_retry,
|
||||
'failed_events': failed_events,
|
||||
'runtime': status,
|
||||
}, ensure_ascii=False, indent=2))
|
||||
|
||||
if decision == 'rollback':
|
||||
sys.exit(2)
|
||||
if decision == 'pause':
|
||||
sys.exit(1)
|
||||
PY
|
||||
33
scripts/gateway_closure_rollback.sh
Normal file
33
scripts/gateway_closure_rollback.sh
Normal file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="${BASE_URL:-http://127.0.0.1:8080}"
|
||||
|
||||
need() {
|
||||
command -v "$1" >/dev/null 2>&1 || {
|
||||
echo "missing required command: $1" >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
need curl
|
||||
need python3
|
||||
|
||||
echo "[1/3] pause gateway runtime"
|
||||
curl -fsS -X POST "$BASE_URL/internal/supply-intelligence/gateway/runtime/pause"
|
||||
echo
|
||||
|
||||
echo "[2/3] fetch runtime status for rollback assessment"
|
||||
status=$(curl -fsS "$BASE_URL/internal/supply-intelligence/gateway/runtime-status")
|
||||
echo "$status"
|
||||
|
||||
echo "[3/3] operator checklist"
|
||||
python3 <<'PY'
|
||||
print('''Manual rollback checklist:
|
||||
1. Confirm runtime paused and record pending_retry_events / failed_events.
|
||||
2. Inspect GET /internal/supply-intelligence/gateway/package-changes for the affected event IDs.
|
||||
3. If a replacement package is prepared, publish the replacement package-event and verify admission-state.
|
||||
4. If the bad event must remain blocked, keep runtime paused until manual remediation is completed.
|
||||
5. After remediation, call POST /internal/supply-intelligence/gateway/runtime/resume and rerun gateway_closure_inspect.sh.
|
||||
''')
|
||||
PY
|
||||
76
scripts/gateway_closure_smoke.sh
Normal file
76
scripts/gateway_closure_smoke.sh
Normal file
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="${BASE_URL:-http://127.0.0.1:8080}"
|
||||
PLATFORM="${PLATFORM:-openai}"
|
||||
MODEL="${MODEL:-gpt-4.1-mini}"
|
||||
EVENT_ID="${EVENT_ID:-evt-smoke-$(date +%s)}"
|
||||
OCCURRED_AT="${OCCURRED_AT:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}"
|
||||
CANDIDATE_STATUS_EXPECTED="${CANDIDATE_STATUS_EXPECTED:-published}"
|
||||
|
||||
need() {
|
||||
command -v "$1" >/dev/null 2>&1 || {
|
||||
echo "missing required command: $1" >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
need curl
|
||||
need python3
|
||||
|
||||
json_get() {
|
||||
local expr="$1"
|
||||
python3 -c "import json,sys; data=json.load(sys.stdin); print($expr)"
|
||||
}
|
||||
|
||||
echo "[1/4] publish package event"
|
||||
publish_resp=$(curl -fsS -X POST "$BASE_URL/internal/supply-intelligence/publish/package-event" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"event_id\":\"$EVENT_ID\",\"platform\":\"$PLATFORM\",\"model\":\"$MODEL\",\"occurred_at\":\"$OCCURRED_AT\"}")
|
||||
echo "$publish_resp"
|
||||
|
||||
publish_event_id=$(printf '%s' "$publish_resp" | json_get "data['event']['event_id']")
|
||||
[ "$publish_event_id" = "$EVENT_ID" ] || {
|
||||
echo "publish returned unexpected event id: $publish_event_id" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "[2/4] trigger consume-once"
|
||||
consume_resp=$(curl -fsS -X POST "$BASE_URL/internal/supply-intelligence/gateway/consume-once" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"consumer":"gateway"}')
|
||||
echo "$consume_resp"
|
||||
|
||||
consume_items=$(printf '%s' "$consume_resp" | json_get "len(data['items'])")
|
||||
[ "$consume_items" -ge 1 ] || {
|
||||
echo "consume-once returned no items" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "[3/4] verify package change list includes event"
|
||||
changes_resp=$(curl -fsS "$BASE_URL/internal/supply-intelligence/gateway/package-changes")
|
||||
echo "$changes_resp"
|
||||
found=$(printf '%s' "$changes_resp" | python3 -c "import json,sys; data=json.load(sys.stdin); print(any(item.get('event_id') == '$EVENT_ID' for item in data.get('items', [])))")
|
||||
[ "$found" = "True" ] || {
|
||||
echo "package change list missing event $EVENT_ID" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "[4/4] verify admission-state reflects publish/consume state"
|
||||
admission_resp=$(curl -fsS "$BASE_URL/internal/supply-intelligence/models/$PLATFORM/$MODEL/admission-state")
|
||||
echo "$admission_resp"
|
||||
candidate_status=$(printf '%s' "$admission_resp" | json_get "data['candidate']['status'] if data.get('candidate') else ''")
|
||||
gateway_status=$(printf '%s' "$admission_resp" | json_get "data.get('gateway_sync_status', '')")
|
||||
[ "$candidate_status" = "$CANDIDATE_STATUS_EXPECTED" ] || {
|
||||
echo "unexpected candidate status: $candidate_status" >&2
|
||||
exit 1
|
||||
}
|
||||
case "$gateway_status" in
|
||||
applied|pending|failed) ;;
|
||||
*)
|
||||
echo "unexpected gateway sync status: $gateway_status" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "gateway closure smoke passed: event=$EVENT_ID candidate_status=$candidate_status gateway_sync_status=$gateway_status"
|
||||
55
scripts/review/HERMES_DAILY_REVIEW_PROMPT.md
Normal file
55
scripts/review/HERMES_DAILY_REVIEW_PROMPT.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Hermes Daily Review Prompt
|
||||
|
||||
目标:基于当前仓库真实状态,对 `supply-intelligence` 做一次严谨的日度 review,并输出专业报告与 Hermes 优化建议。
|
||||
|
||||
执行要求:
|
||||
|
||||
1. 只基于真实事实,不基于记忆或假设。
|
||||
2. 这个 review 默认**不更新任何 TASKS/GOALS 状态**,只产出报告与建议。
|
||||
3. 如果后续用户明确要求同步任务状态,而且本项目已经引入项目内 `TASKS.md` / `GOALS.md`:
|
||||
- 只能写项目内任务文件,禁止写 `~/.openclaw/workspace/TASKS.md` 与 `~/.openclaw/workspace/GOALS.md`
|
||||
- 写回前必须先执行:
|
||||
- `bash /home/long/.openclaw/workspace/scripts/preflight_task_write_guard.sh project-review /home/long/project/supply-intelligence /home/long/project/supply-intelligence/TASKS.md`
|
||||
- 守卫失败时立即停止,不得继续 `edit` 或 `write`
|
||||
4. 必须先检查:
|
||||
- `git status --short`
|
||||
- 最近提交记录
|
||||
- 当前关键文档与脚本目录
|
||||
- 当前可执行的验证命令
|
||||
5. 优先执行非破坏性验证:
|
||||
- `go build ./...`
|
||||
- `go test ./...`
|
||||
- 如果有更贴近真实链路的校验脚本,也可以补充执行
|
||||
6. 如果命令失败,记录精确失败点、失败命令、错误摘要,不得模糊描述。
|
||||
7. 这个 review 任务只产出报告与建议,不改业务代码;如果发现必须立即修复的问题,只在报告中列出。
|
||||
|
||||
输出文件:
|
||||
|
||||
1. 每日 review 报告:
|
||||
- 路径:`reports/hermes/YYYY-MM-DD-review.md`
|
||||
- 如果当天文件已存在,则覆盖为最新真实状态
|
||||
2. Hermes 优化建议文档:
|
||||
- 路径:`reports/hermes/HERMES_OPTIMIZATION_SUGGESTIONS.md`
|
||||
- 追加或更新当天小节
|
||||
|
||||
`YYYY-MM-DD-review.md` 必须包含:
|
||||
|
||||
- 标题与时间
|
||||
- Executive Summary
|
||||
- 当前真实完成度判断
|
||||
- 今日验证证据
|
||||
- 已完成事项
|
||||
- 进行中事项
|
||||
- 阻塞项与风险
|
||||
- 发现的文档/实现偏差
|
||||
- 下一步最值得推进的 3 件事
|
||||
|
||||
`HERMES_OPTIMIZATION_SUGGESTIONS.md` 必须包含:
|
||||
|
||||
- 日期
|
||||
- 本次 review 暴露出的 Hermes 工作方式问题
|
||||
- 每个问题的优化建议
|
||||
- 优先级(P0/P1/P2)
|
||||
- 建议的验证方式
|
||||
|
||||
完成后,在最终回复中只做简洁摘要,并明确写出生成/更新了哪些文件。
|
||||
106
scripts/run_migrations.sh
Normal file
106
scripts/run_migrations.sh
Normal file
@@ -0,0 +1,106 @@
|
||||
#!/bin/bash
|
||||
# Migration runner for supply-intelligence
|
||||
# Supports both in-memory mode (no DB) and PostgreSQL mode (via DATABASE_URL)
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/run_migrations.sh # runs all pending migrations
|
||||
# ./scripts/run_migrations.sh --status # show migration status
|
||||
# ./scripts/run_migrations.sh --baseline <id> # baseline an existing DB
|
||||
|
||||
set -e
|
||||
|
||||
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
MIGRATIONS_DIR="${PROJECT_DIR}/migrations"
|
||||
DATABASE_URL="${DATABASE_URL:-}"
|
||||
|
||||
# Resolve absolute path to migrations folder
|
||||
MIGRATIONS_DIR="$(cd "$MIGRATIONS_DIR" && pwd)"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
log_error() { echo -e "${RED}[ERR]${NC} $*" >&2; }
|
||||
|
||||
run_postgres_migrations() {
|
||||
if [ -z "$DATABASE_URL" ]; then
|
||||
log_error "DATABASE_URL not set. Cannot run SQL migrations."
|
||||
log_info "Set DATABASE_URL to run PostgreSQL migrations."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local conn="$DATABASE_URL"
|
||||
local db_name
|
||||
db_name=$(echo "$conn" | sed -E 's|.*/([^?]+)(\?.*)?|\1|')
|
||||
|
||||
echo "CREATE TABLE IF NOT EXISTS schema_history (
|
||||
installed_rank INTEGER PRIMARY KEY,
|
||||
version VARCHAR(50),
|
||||
description VARCHAR(200),
|
||||
type VARCHAR(20),
|
||||
script VARCHAR(1000),
|
||||
checksum BIGINT,
|
||||
installed_by VARCHAR(100),
|
||||
installed_on TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
execution_time_ms BIGINT,
|
||||
success SMALLINT
|
||||
);" | PGPASSWORD="${PGPASSWORD:-}" psql -h "${PGHOST:-localhost}" -U "${PGUSER:-supply}" -d "$db_name" 2>/dev/null || true
|
||||
|
||||
log_info "PostgreSQL migration runner ready"
|
||||
log_info "DB: $db_name"
|
||||
log_info "Migrations dir: $MIGRATIONS_DIR"
|
||||
|
||||
local count=0
|
||||
for f in "$MIGRATIONS_DIR"/*.sql; do
|
||||
[ -e "$f" ] || continue
|
||||
echo " $(basename "$f")"
|
||||
count=$((count + 1))
|
||||
done
|
||||
log_info "Found $count SQL migration file(s)"
|
||||
}
|
||||
|
||||
run_inmemory_migrations() {
|
||||
log_info "In-memory mode: migrations are embedded in application startup"
|
||||
log_info "Set DATABASE_URL to enable PostgreSQL migration runner"
|
||||
echo ""
|
||||
echo "Available migrations in $MIGRATIONS_DIR:"
|
||||
local count=0
|
||||
for f in "$MIGRATIONS_DIR"/*.sql; do
|
||||
[ -e "$f" ] || continue
|
||||
echo " $(basename "$f")"
|
||||
count=$((count + 1))
|
||||
done
|
||||
log_info "Total: $count migration(s)"
|
||||
}
|
||||
|
||||
main() {
|
||||
case "${1:-}" in
|
||||
--status)
|
||||
if [ -n "$DATABASE_URL" ]; then
|
||||
log_info "PostgreSQL mode"
|
||||
run_postgres_migrations
|
||||
else
|
||||
log_info "In-memory mode (no DATABASE_URL)"
|
||||
run_inmemory_migrations
|
||||
fi
|
||||
;;
|
||||
--baseline)
|
||||
log_warn "Baseline not implemented — use golang-migrate or flyway"
|
||||
;;
|
||||
*)
|
||||
if [ -n "$DATABASE_URL" ]; then
|
||||
log_info "Running PostgreSQL migrations..."
|
||||
run_postgres_migrations
|
||||
else
|
||||
log_info "No DATABASE_URL — showing available migrations"
|
||||
run_inmemory_migrations
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
47
scripts/sub2api-bridge.sh
Normal file
47
scripts/sub2api-bridge.sh
Normal file
@@ -0,0 +1,47 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
SUPPLY_URL="${SUPPLY_URL:-http://127.0.0.1:8081}"
|
||||
CONSUMER="${CONSUMER:-sub2api-bridge}"
|
||||
CURSOR=""
|
||||
|
||||
# Create bridge log table in sub2api database
|
||||
docker exec sub2api-postgres psql -U sub2api -d sub2api -c "
|
||||
CREATE TABLE IF NOT EXISTS supply_bridge_log (
|
||||
id SERIAL PRIMARY KEY,
|
||||
event_id TEXT NOT NULL,
|
||||
package_id BIGINT,
|
||||
status TEXT,
|
||||
result TEXT,
|
||||
detail TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);" 2>/dev/null || true
|
||||
|
||||
while true; do
|
||||
RESP=$(curl -fsS -X POST "${SUPPLY_URL}/internal/supply-intelligence/gateway/consume-once?consumer=${CONSUMER}&cursor=${CURSOR}" 2>/dev/null || echo '{}')
|
||||
NEXT_CURSOR=$(echo "$RESP" | jq -r '.next_cursor // empty')
|
||||
ITEMS_LEN=$(echo "$RESP" | jq '.items | length')
|
||||
|
||||
if [ "$ITEMS_LEN" -eq 0 ]; then
|
||||
sleep 10
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "$RESP" | jq -c '.items[]' | while read -r item; do
|
||||
EVENT_ID=$(echo "$item" | jq -r '.event_id')
|
||||
PKG_ID=$(echo "$item" | jq -r '.package_id')
|
||||
STATUS=$(echo "$item" | jq -r '.gateway_sync_status')
|
||||
RESULT=$(echo "$item" | jq -r '.result')
|
||||
DETAIL=$(echo "$item" | jq -r '.detail // empty')
|
||||
echo "$(date -Is) bridge event=$EVENT_ID package=$PKG_ID status=$STATUS result=$RESULT"
|
||||
|
||||
# Insert into sub2api database
|
||||
docker exec sub2api-postgres psql -U sub2api -d sub2api -c \
|
||||
"INSERT INTO supply_bridge_log (event_id, package_id, status, result, detail) VALUES ('$EVENT_ID', $PKG_ID, '$STATUS', '$RESULT', '$DETAIL');" 2>/dev/null || true
|
||||
done
|
||||
|
||||
CURSOR="$NEXT_CURSOR"
|
||||
if [ -z "$CURSOR" ]; then
|
||||
sleep 10
|
||||
fi
|
||||
done
|
||||
Reference in New Issue
Block a user