test(personal): close personal knowledge pilot gates
This commit is contained in:
@@ -367,6 +367,10 @@ contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/control
|
||||
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "仅支持 txt/md/markdown/pdf/doc/docx/xls/xlsx/ppt/pptx/图片文件"
|
||||
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "/embeddings"
|
||||
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "aihr_knowledge_gap"
|
||||
contains backend/script/sql/aihr_personal_knowledge_mysql8.sql "aihr_personal_fragment"
|
||||
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/personal/service/PersonalVectorStore.java "owner_user_id"
|
||||
contains mobile-uni/src/pages/user/assistant/index.vue "我的资料"
|
||||
contains scripts/personal-assistant-smoke.sh "PERSONAL_ITEM_NOT_FOUND"
|
||||
contains backend/script/sql/aihr_knowledge_mysql8.sql "embedding_json"
|
||||
contains frontend/src/views/knowledge/sop.vue "上传文档"
|
||||
contains frontend/src/views/knowledge/sop.vue ".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx"
|
||||
|
||||
Executable
+170
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
API_URL="${AIHR_PERSONAL_API_URL:-https://wygj-api.localhost}"
|
||||
QDRANT_URL="${AIHR_QDRANT_URL:-http://127.0.0.1:6333}"
|
||||
QDRANT_COLLECTION="${AIHR_PERSONAL_QDRANT_COLLECTION:-aihr_personal_knowledge}"
|
||||
DB_CONTAINER="${AIHR_DB_CONTAINER:-wygj-mysql}"
|
||||
DB_NAME="${AIHR_DB_NAME:-ry-vue}"
|
||||
REDIS_CONTAINER="${AIHR_REDIS_CONTAINER:-wygj-redis}"
|
||||
DOCKER_NETWORK="${AIHR_DOCKER_NETWORK:-wygj_default}"
|
||||
PHONE_A="13900009111"
|
||||
PHONE_B="13900009222"
|
||||
SMS_CODE="${AIHR_SMS_DEV_CODE:-123456}"
|
||||
TITLE="personal-assistant-smoke-isolation"
|
||||
CONTENT="个人助理隔离烟测唯一标记,物业催费沟通资料。"
|
||||
|
||||
TOKEN_A=""
|
||||
CLIENT_A=""
|
||||
TOKEN_B=""
|
||||
CLIENT_B=""
|
||||
USER_A=""
|
||||
USER_B=""
|
||||
ITEM_ID=""
|
||||
|
||||
fail() { echo "FAIL: $*" >&2; exit 1; }
|
||||
need() { command -v "$1" >/dev/null 2>&1 || fail "missing command: $1"; }
|
||||
expect_code() {
|
||||
local actual="$1" expected="$2" label="$3"
|
||||
[[ "$actual" == "$expected" ]] || fail "$label expected=$expected actual=$actual"
|
||||
}
|
||||
|
||||
mysql() {
|
||||
docker exec "$DB_CONTAINER" mysql -uroot -proot --default-character-set=utf8mb4 -N -B -D "$DB_NAME" -e "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
local exit_code=$?
|
||||
local oss_rows oss_id object_key
|
||||
set +e
|
||||
[[ -n "$USER_A" ]] || USER_A="$(mysql "select user_id from sys_user where phonenumber='$PHONE_A' order by user_id desc limit 1" | head -1)"
|
||||
[[ -n "$USER_B" ]] || USER_B="$(mysql "select user_id from sys_user where phonenumber='$PHONE_B' order by user_id desc limit 1" | head -1)"
|
||||
for uid in "$USER_A" "$USER_B"; do
|
||||
[[ "$uid" =~ ^[0-9]+$ ]] || continue
|
||||
curl -fsS -X POST "$QDRANT_URL/collections/$QDRANT_COLLECTION/points/delete?wait=true" \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data "{\"filter\":{\"must\":[{\"key\":\"tenant_id\",\"match\":{\"value\":\"000000\"}},{\"key\":\"owner_user_id\",\"match\":{\"value\":$uid}}]}}" >/dev/null 2>&1 || true
|
||||
done
|
||||
oss_rows="$(mysql "select o.oss_id,o.file_name from sys_oss o join aihr_personal_item i on i.oss_id=o.oss_id where i.title='$TITLE' and i.owner_user_id in (coalesce(nullif('$USER_A',''),0),coalesce(nullif('$USER_B',''),0))" || true)"
|
||||
while IFS=$'\t' read -r oss_id object_key; do
|
||||
[[ "$oss_id" =~ ^[0-9]+$ && -n "$object_key" ]] || continue
|
||||
docker run --rm --network "$DOCKER_NETWORK" --entrypoint /bin/sh minio/mc -c \
|
||||
"mc alias set local http://wygj-minio:9000 ruoyi ruoyi123 >/dev/null && mc rm --force 'local/ruoyi/$object_key' >/dev/null" \
|
||||
>/dev/null 2>&1 || true
|
||||
mysql "delete from sys_oss where oss_id=$oss_id and file_name='$object_key'" >/dev/null 2>&1 || true
|
||||
done <<<"$oss_rows"
|
||||
mysql "
|
||||
delete f from aihr_personal_fragment f join aihr_personal_item i on i.id=f.item_id
|
||||
where i.title='$TITLE' and i.owner_user_id in (coalesce(nullif('$USER_A',''),0),coalesce(nullif('$USER_B',''),0));
|
||||
delete j from aihr_personal_cleanup_job j join aihr_personal_item i on i.id=j.item_id
|
||||
where i.title='$TITLE' and i.owner_user_id in (coalesce(nullif('$USER_A',''),0),coalesce(nullif('$USER_B',''),0));
|
||||
delete from aihr_personal_item where title='$TITLE'
|
||||
and owner_user_id in (coalesce(nullif('$USER_A',''),0),coalesce(nullif('$USER_B',''),0));
|
||||
delete m from aihr_personal_chat_message m join aihr_personal_chat_session s on s.id=m.session_id
|
||||
where s.owner_user_id in (coalesce(nullif('$USER_A',''),0),coalesce(nullif('$USER_B',''),0));
|
||||
delete from aihr_personal_chat_session
|
||||
where owner_user_id in (coalesce(nullif('$USER_A',''),0),coalesce(nullif('$USER_B',''),0));
|
||||
delete from aihr_personal_space
|
||||
where owner_user_id in (coalesce(nullif('$USER_A',''),0),coalesce(nullif('$USER_B',''),0));
|
||||
delete from sys_user_role where user_id in (coalesce(nullif('$USER_A',''),0),coalesce(nullif('$USER_B',''),0));
|
||||
delete from sys_user_post where user_id in (coalesce(nullif('$USER_A',''),0),coalesce(nullif('$USER_B',''),0));
|
||||
delete from sys_user where phonenumber in ('$PHONE_A','$PHONE_B') and remark='移动端短信自动注册';
|
||||
delete from sys_logininfor where user_name in ('$PHONE_A','$PHONE_B');
|
||||
" >/dev/null 2>&1 || true
|
||||
docker exec "$REDIS_CONTAINER" redis-cli -a ruoyi123 DEL \
|
||||
"global:captcha_codes:$PHONE_A" "global:captcha_codes:$PHONE_B" >/dev/null 2>&1 || true
|
||||
for phone in "$PHONE_A" "$PHONE_B"; do
|
||||
docker exec "$REDIS_CONTAINER" sh -c \
|
||||
"redis-cli -a ruoyi123 --scan --pattern '*resource/sms/code:$phone*' 2>/dev/null | xargs -r redis-cli -a ruoyi123 DEL" \
|
||||
>/dev/null 2>&1 || true
|
||||
done
|
||||
return "$exit_code"
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
api() {
|
||||
local token="$1" client="$2" method="$3" path="$4" body="${5:-}"
|
||||
local args=(-kfsS -X "$method" "$API_URL$path" -H "Authorization: Bearer $token" -H "clientid: $client")
|
||||
[[ -z "$body" ]] || args+=(-H 'Content-Type: application/json' --data "$body")
|
||||
curl "${args[@]}"
|
||||
}
|
||||
|
||||
login() {
|
||||
local phone="$1" response
|
||||
response="$(curl -kfsS "$API_URL/resource/sms/code?phonenumber=$phone")"
|
||||
expect_code "$(jq -r '.code' <<<"$response")" "200" "send sms code"
|
||||
response="$(curl -kfsS -X POST "$API_URL/auth/mobile/sms-login" -H 'Content-Type: application/json' \
|
||||
--data "{\"tenantId\":\"000000\",\"phonenumber\":\"$phone\",\"smsCode\":\"$SMS_CODE\"}")"
|
||||
expect_code "$(jq -r '.code' <<<"$response")" "200" "mobile sms login"
|
||||
jq -er '.data.access_token, .data.client_id' <<<"$response" | paste -sd ' ' -
|
||||
}
|
||||
|
||||
need curl
|
||||
need jq
|
||||
need docker
|
||||
curl -kfsS "$API_URL/auth/tenant/list" | jq -e '.code == 200' >/dev/null || fail "backend health is not UP: $API_URL"
|
||||
docker ps --format '{{.Names}}' | grep -qx "$DB_CONTAINER" || fail "database container not running: $DB_CONTAINER"
|
||||
docker ps --format '{{.Names}}' | grep -qx "$REDIS_CONTAINER" || fail "redis container not running: $REDIS_CONTAINER"
|
||||
mysql "select 1 from aihr_personal_item limit 1" >/dev/null || fail "personal knowledge schema missing; run ./scripts/reset-dev-db.sh"
|
||||
|
||||
cleanup
|
||||
read -r TOKEN_A CLIENT_A <<<"$(login "$PHONE_A")"
|
||||
read -r TOKEN_B CLIENT_B <<<"$(login "$PHONE_B")"
|
||||
USER_A="$(mysql "select user_id from sys_user where phonenumber='$PHONE_A' order by user_id desc limit 1")"
|
||||
USER_B="$(mysql "select user_id from sys_user where phonenumber='$PHONE_B' order by user_id desc limit 1")"
|
||||
[[ "$USER_A" =~ ^[0-9]+$ && "$USER_B" =~ ^[0-9]+$ && "$USER_A" != "$USER_B" ]] || fail "smoke users were not created independently"
|
||||
|
||||
created="$(api "$TOKEN_A" "$CLIENT_A" POST /api/aihr/personal-assistant/items/text \
|
||||
"{\"title\":\"$TITLE\",\"content\":\"$CONTENT\",\"tags\":[\"smoke\"]}")"
|
||||
expect_code "$(jq -r '.code' <<<"$created")" "200" "A create text item"
|
||||
ITEM_ID="$(jq -er '.data.itemId' <<<"$created")"
|
||||
|
||||
for _ in {1..40}; do
|
||||
detail="$(api "$TOKEN_A" "$CLIENT_A" GET "/api/aihr/personal-assistant/items/$ITEM_ID")"
|
||||
[[ "$(jq -r '.data.status // empty' <<<"$detail")" == "READY" ]] && break
|
||||
sleep 1
|
||||
done
|
||||
expect_code "$(jq -r '.data.status // empty' <<<"$detail")" "READY" "A text item processing"
|
||||
expect_code "$(jq -r '.data.title // empty' <<<"$detail")" "$TITLE" "A item detail"
|
||||
|
||||
search_a="$(api "$TOKEN_A" "$CLIENT_A" POST /api/aihr/personal-assistant/search \
|
||||
"{\"queryText\":\"隔离烟测\",\"scope\":[\"PERSONAL\"],\"itemIds\":[$ITEM_ID],\"limit\":10}")"
|
||||
expect_code "$(jq -r '.code' <<<"$search_a")" "200" "A search"
|
||||
[[ "$(jq --arg item "$ITEM_ID" '[.data.hits[] | select((.itemId | tostring) == $item)] | length' <<<"$search_a")" -ge 1 ]] || {
|
||||
jq '{code,msg,data}' <<<"$search_a" >&2
|
||||
fail "A search did not return own item"
|
||||
}
|
||||
|
||||
for operation in detail download retry delete; do
|
||||
case "$operation" in
|
||||
detail) response="$(api "$TOKEN_B" "$CLIENT_B" GET "/api/aihr/personal-assistant/items/$ITEM_ID" || true)" ;;
|
||||
download) response="$(api "$TOKEN_B" "$CLIENT_B" GET "/api/aihr/personal-assistant/items/$ITEM_ID/download-url" || true)" ;;
|
||||
retry) response="$(api "$TOKEN_B" "$CLIENT_B" POST "/api/aihr/personal-assistant/items/$ITEM_ID/retry" || true)" ;;
|
||||
delete) response="$(api "$TOKEN_B" "$CLIENT_B" DELETE "/api/aihr/personal-assistant/items/$ITEM_ID" || true)" ;;
|
||||
esac
|
||||
expect_code "$(jq -r '.msg // empty' <<<"$response")" "PERSONAL_ITEM_NOT_FOUND" "B $operation A item"
|
||||
done
|
||||
|
||||
search_b="$(api "$TOKEN_B" "$CLIENT_B" POST /api/aihr/personal-assistant/search \
|
||||
"{\"queryText\":\"隔离烟测\",\"scope\":[\"PERSONAL\"],\"itemIds\":[$ITEM_ID],\"limit\":10}")"
|
||||
expect_code "$(jq -r '.code' <<<"$search_b")" "200" "B itemIds search"
|
||||
expect_code "$(jq '.data.hits | length' <<<"$search_b")" "0" "B itemIds search isolation"
|
||||
|
||||
for blocked_url in 'http://127.0.0.1/admin' 'http://169.254.169.254/latest/meta-data/'; do
|
||||
response="$(api "$TOKEN_A" "$CLIENT_A" POST /api/aihr/personal-assistant/items/url \
|
||||
"{\"url\":\"$blocked_url\",\"title\":\"$TITLE\"}" || true)"
|
||||
expect_code "$(jq -r '.msg // empty' <<<"$response")" "PERSONAL_URL_BLOCKED" "SSRF $blocked_url"
|
||||
done
|
||||
|
||||
deleted1="$(api "$TOKEN_A" "$CLIENT_A" DELETE "/api/aihr/personal-assistant/items/$ITEM_ID")"
|
||||
deleted2="$(api "$TOKEN_A" "$CLIENT_A" DELETE "/api/aihr/personal-assistant/items/$ITEM_ID")"
|
||||
expect_code "$(jq -r '.code' <<<"$deleted1")" "200" "A delete"
|
||||
expect_code "$(jq -r '.data.cleanupJobId' <<<"$deleted2")" "$(jq -r '.data.cleanupJobId' <<<"$deleted1")" "idempotent delete job"
|
||||
response="$(api "$TOKEN_A" "$CLIENT_A" GET "/api/aihr/personal-assistant/items/$ITEM_ID" || true)"
|
||||
expect_code "$(jq -r '.msg // empty' <<<"$response")" "PERSONAL_ITEM_NOT_FOUND" "A item hidden immediately"
|
||||
response="$(api "$TOKEN_A" "$CLIENT_A" POST /api/aihr/personal-assistant/search \
|
||||
"{\"queryText\":\"隔离烟测\",\"scope\":[\"PERSONAL\"],\"itemIds\":[$ITEM_ID],\"limit\":10}")"
|
||||
expect_code "$(jq '.data.hits | length' <<<"$response")" "0" "deleted item search"
|
||||
|
||||
echo "PASS: personal assistant isolation, SSRF, readiness and deletion gates"
|
||||
Reference in New Issue
Block a user