feat: harden storyforge runtime and repo boundary
This commit is contained in:
56
scripts/check_repo_baseline.sh
Executable file
56
scripts/check_repo_baseline.sh
Executable file
@@ -0,0 +1,56 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ROOT="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
|
||||
|
||||
need_cmd() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "missing required command: $1" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
need_cmd python3
|
||||
need_cmd docker
|
||||
need_cmd node
|
||||
|
||||
cd "$ROOT"
|
||||
|
||||
echo "[1/5] compile collector-service"
|
||||
python3 -m compileall collector-service/app >/dev/null
|
||||
|
||||
echo "[2/5] validate docker compose"
|
||||
docker compose config >/dev/null
|
||||
|
||||
echo "[3/5] validate n8n workflows"
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import pathlib
|
||||
|
||||
for path in sorted(pathlib.Path("n8n/workflows").glob("*.json")):
|
||||
with path.open() as handle:
|
||||
json.load(handle)
|
||||
print(f"workflow ok: {path.name}")
|
||||
PY
|
||||
|
||||
echo "[4/5] validate web scripts"
|
||||
for file in web/storyforge-web-v4/assets/app.js web/storyforge-web-v4/assets/storyforge-*.js; do
|
||||
node --check "$file"
|
||||
done
|
||||
node --check scripts/douyin-browser-capture/control_panel.mjs
|
||||
|
||||
if [ "${STORYFORGE_SKIP_ANDROID:-0}" = "1" ]; then
|
||||
echo "[5/5] skip android compile (STORYFORGE_SKIP_ANDROID=1)"
|
||||
else
|
||||
if command -v java >/dev/null 2>&1; then
|
||||
echo "[5/5] compile android debug kotlin"
|
||||
(
|
||||
cd android-app
|
||||
./gradlew :app:compileDebugKotlin >/dev/null
|
||||
)
|
||||
else
|
||||
echo "[5/5] skip android compile (java not installed)"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "baseline checks passed"
|
||||
@@ -43,6 +43,8 @@ The control panel stores each run under:
|
||||
|
||||
`/Users/kris/code/StoryForge-gitea/output/playwright/douyin/control-panel`
|
||||
|
||||
The StoryForge token field is session-scoped in the browser and is not written back into the saved form values, so it will not be refilled from localStorage on the next launch.
|
||||
|
||||
## What it captures
|
||||
|
||||
- current profile page JSON blobs extracted from `<script>` tags
|
||||
|
||||
@@ -1157,6 +1157,7 @@ function renderPage(mode = "full") {
|
||||
const form = document.getElementById("capture-form");
|
||||
const storageKey = "storyforge-douyin-control-panel";
|
||||
const sessionStorageKey = "storyforge-douyin-workbench-session";
|
||||
const tokenStorageKey = "storyforge-douyin-control-panel-token";
|
||||
const workbenchSessionEl = document.getElementById("workbench-session");
|
||||
const tokenDetailsEl = document.getElementById("token-details");
|
||||
const tokenInputEl = document.getElementById("token");
|
||||
@@ -1208,7 +1209,8 @@ function renderPage(mode = "full") {
|
||||
selectedSnapshotDetail: null,
|
||||
similarSearchDetail: null,
|
||||
loadingAccountId: "",
|
||||
lastAnalysisMessage: ""
|
||||
lastAnalysisMessage: "",
|
||||
accountSelectionToken: 0
|
||||
};
|
||||
|
||||
function escapeHtml(value) {
|
||||
@@ -1272,7 +1274,7 @@ function renderPage(mode = "full") {
|
||||
const backendUrl = normalizeBackendUrl(document.getElementById("backend-url").value);
|
||||
const username = document.getElementById("username").value.trim();
|
||||
const password = document.getElementById("password").value;
|
||||
const token = tokenInputEl.value.trim();
|
||||
const token = tokenInputEl.value.trim() || getStoredCaptureToken();
|
||||
return {
|
||||
backendUrl,
|
||||
username,
|
||||
@@ -1281,9 +1283,32 @@ function renderPage(mode = "full") {
|
||||
};
|
||||
}
|
||||
|
||||
function getStoredCaptureToken() {
|
||||
try {
|
||||
return sessionStorage.getItem(tokenStorageKey) || "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function persistCaptureToken(token) {
|
||||
try {
|
||||
if (token) {
|
||||
sessionStorage.setItem(tokenStorageKey, token);
|
||||
} else {
|
||||
sessionStorage.removeItem(tokenStorageKey);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function updateTokenUi() {
|
||||
const token = tokenInputEl.value.trim();
|
||||
tokenSummaryEl.textContent = token ? "已填写 Token" : "未填写 Token";
|
||||
const storedToken = getStoredCaptureToken();
|
||||
tokenSummaryEl.textContent = token
|
||||
? "已填写 Token"
|
||||
: storedToken
|
||||
? "Token 已保存到当前会话"
|
||||
: "未填写 Token";
|
||||
if (!token && document.activeElement !== tokenInputEl) {
|
||||
tokenDetailsEl.open = false;
|
||||
}
|
||||
@@ -1291,7 +1316,8 @@ function renderPage(mode = "full") {
|
||||
|
||||
function persistWorkbenchSession(session) {
|
||||
workbenchState.session = session;
|
||||
localStorage.setItem(sessionStorageKey, JSON.stringify(session));
|
||||
sessionStorage.setItem(sessionStorageKey, JSON.stringify(session));
|
||||
localStorage.removeItem(sessionStorageKey);
|
||||
renderWorkbenchSession();
|
||||
}
|
||||
|
||||
@@ -1310,7 +1336,9 @@ function renderPage(mode = "full") {
|
||||
workbenchState.selectedSnapshotDetail = null;
|
||||
workbenchState.similarSearchDetail = null;
|
||||
workbenchState.lastAnalysisMessage = "";
|
||||
sessionStorage.removeItem(sessionStorageKey);
|
||||
localStorage.removeItem(sessionStorageKey);
|
||||
persistCaptureToken("");
|
||||
renderWorkbenchSession();
|
||||
renderAccountList();
|
||||
renderWorkspace();
|
||||
@@ -1318,9 +1346,21 @@ function renderPage(mode = "full") {
|
||||
|
||||
function loadWorkbenchSession() {
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem(sessionStorageKey) || "null");
|
||||
const sessionRaw = sessionStorage.getItem(sessionStorageKey);
|
||||
if (sessionRaw) {
|
||||
const saved = JSON.parse(sessionRaw);
|
||||
if (saved && saved.token && saved.backendUrl) {
|
||||
workbenchState.session = saved;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const legacyRaw = localStorage.getItem(sessionStorageKey);
|
||||
if (!legacyRaw) return;
|
||||
const saved = JSON.parse(legacyRaw);
|
||||
if (saved && saved.token && saved.backendUrl) {
|
||||
workbenchState.session = saved;
|
||||
sessionStorage.setItem(sessionStorageKey, JSON.stringify(saved));
|
||||
localStorage.removeItem(sessionStorageKey);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
@@ -1838,6 +1878,8 @@ function renderPage(mode = "full") {
|
||||
return;
|
||||
}
|
||||
const preserveFeedback = options.preserveFeedback === true;
|
||||
const selectionToken = (workbenchState.accountSelectionToken || 0) + 1;
|
||||
workbenchState.accountSelectionToken = selectionToken;
|
||||
workbenchState.selectedAccountId = accountId;
|
||||
workbenchState.loadingAccountId = accountId;
|
||||
if (!preserveFeedback) {
|
||||
@@ -1851,6 +1893,9 @@ function renderPage(mode = "full") {
|
||||
storyforgeFetch("/v2/douyin/accounts/" + encodeURIComponent(accountId) + "/snapshots").catch(() => []),
|
||||
storyforgeFetch("/v2/douyin/accounts/" + encodeURIComponent(accountId) + "/videos?limit=1000").catch(() => ({ items: [], meta: {}, top_scored_video_ids: [], latest_video_ids: [], high_score_threshold: 60 }))
|
||||
]);
|
||||
if (selectionToken !== workbenchState.accountSelectionToken) {
|
||||
return;
|
||||
}
|
||||
workbenchState.selectedWorkspace = results[0];
|
||||
workbenchState.snapshots = safeArray(results[1]);
|
||||
workbenchState.videoItems = safeArray(results[2]?.items);
|
||||
@@ -1871,10 +1916,15 @@ function renderPage(mode = "full") {
|
||||
}
|
||||
analysisFeedbackEl.textContent = workbenchState.lastAnalysisMessage || "工作台已加载。";
|
||||
} catch (error) {
|
||||
if (selectionToken !== workbenchState.accountSelectionToken) {
|
||||
return;
|
||||
}
|
||||
analysisFeedbackEl.textContent = "加载工作台失败: " + error.message;
|
||||
} finally {
|
||||
workbenchState.loadingAccountId = "";
|
||||
renderAccountList();
|
||||
if (selectionToken === workbenchState.accountSelectionToken) {
|
||||
workbenchState.loadingAccountId = "";
|
||||
renderAccountList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2001,7 +2051,11 @@ function renderPage(mode = "full") {
|
||||
if (saved.profileUrl) document.getElementById("profile-url").value = saved.profileUrl;
|
||||
if (saved.backendUrl) document.getElementById("backend-url").value = saved.backendUrl;
|
||||
if (saved.username) document.getElementById("username").value = saved.username;
|
||||
if (saved.token) tokenInputEl.value = saved.token;
|
||||
if (saved.token) {
|
||||
persistCaptureToken(saved.token);
|
||||
delete saved.token;
|
||||
localStorage.setItem(storageKey, JSON.stringify(saved));
|
||||
}
|
||||
if (saved.note) document.getElementById("note").value = saved.note;
|
||||
if (saved.maxVideos !== undefined) document.getElementById("max-videos").value = saved.maxVideos;
|
||||
if (saved.syncEnabled !== undefined) document.getElementById("sync-enabled").checked = Boolean(saved.syncEnabled);
|
||||
@@ -2017,7 +2071,6 @@ function renderPage(mode = "full") {
|
||||
profileUrl: payload.profileUrl,
|
||||
backendUrl: payload.backendUrl,
|
||||
username: payload.username,
|
||||
token: payload.token,
|
||||
note: payload.note,
|
||||
maxVideos: payload.maxVideos,
|
||||
syncEnabled: payload.syncEnabled,
|
||||
@@ -2026,6 +2079,7 @@ function renderPage(mode = "full") {
|
||||
allowCreatorCenterFallback: payload.allowCreatorCenterFallback
|
||||
};
|
||||
localStorage.setItem(storageKey, JSON.stringify(saved));
|
||||
persistCaptureToken(payload.token || "");
|
||||
}
|
||||
|
||||
form.addEventListener("submit", async (event) => {
|
||||
@@ -2037,8 +2091,8 @@ function renderPage(mode = "full") {
|
||||
password: document.getElementById("password").value,
|
||||
storyforgeUsername: document.getElementById("username").value.trim(),
|
||||
storyforgePassword: document.getElementById("password").value,
|
||||
token: tokenInputEl.value.trim(),
|
||||
storyforgeToken: tokenInputEl.value.trim(),
|
||||
token: tokenInputEl.value.trim() || getStoredCaptureToken(),
|
||||
storyforgeToken: tokenInputEl.value.trim() || getStoredCaptureToken(),
|
||||
note: document.getElementById("note").value.trim(),
|
||||
maxVideos: document.getElementById("max-videos").value,
|
||||
syncEnabled: document.getElementById("sync-enabled").checked,
|
||||
@@ -2074,7 +2128,11 @@ function renderPage(mode = "full") {
|
||||
tokenInputEl.addEventListener("input", updateTokenUi);
|
||||
tokenDetailsEl.addEventListener("toggle", () => {
|
||||
if (tokenDetailsEl.open) {
|
||||
tokenSummaryEl.textContent = tokenInputEl.value.trim() ? "已填写 Token" : "填写后可跳过密码";
|
||||
tokenSummaryEl.textContent = tokenInputEl.value.trim()
|
||||
? "已填写 Token"
|
||||
: getStoredCaptureToken()
|
||||
? "Token 已保存到当前会话"
|
||||
: "填写后可跳过密码";
|
||||
} else {
|
||||
updateTokenUi();
|
||||
}
|
||||
|
||||
@@ -2,20 +2,33 @@
|
||||
set -eu
|
||||
|
||||
BASE_URL="${STORYFORGE_BASE_URL:-http://127.0.0.1:8081}"
|
||||
USERNAME="${STORYFORGE_USERNAME:-kris}"
|
||||
PASSWORD="${STORYFORGE_PASSWORD:-Asd123456.}"
|
||||
USERNAME="${STORYFORGE_USERNAME:-storyforge-admin}"
|
||||
PASSWORD="${STORYFORGE_PASSWORD:-}"
|
||||
ACCOUNT_ID="${STORYFORGE_SMOKE_ACCOUNT_ID:-dyacct_c2b62842b228406cb48f05fac16fdfdf}"
|
||||
|
||||
if [ -z "$PASSWORD" ]; then
|
||||
echo "STORYFORGE_PASSWORD is required. Export the bootstrap super-admin password before running smoke_business.sh." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
import urllib.request
|
||||
|
||||
base = os.environ.get("BASE_URL", "http://127.0.0.1:8081").rstrip("/")
|
||||
username = os.environ.get("USERNAME", "kris")
|
||||
password = os.environ.get("PASSWORD", "Asd123456.")
|
||||
username = os.environ.get("USERNAME", "storyforge-admin")
|
||||
password = os.environ.get("PASSWORD", "")
|
||||
account_id = os.environ.get("ACCOUNT_ID", "dyacct_c2b62842b228406cb48f05fac16fdfdf")
|
||||
|
||||
if not password:
|
||||
raise SystemExit("STORYFORGE_PASSWORD is required")
|
||||
|
||||
with urllib.request.urlopen(base + "/readyz", timeout=20) as resp:
|
||||
ready = json.load(resp)
|
||||
if not ready.get("ready"):
|
||||
raise SystemExit("collector readyz is not healthy")
|
||||
|
||||
login_req = urllib.request.Request(
|
||||
base + "/v2/auth/login",
|
||||
data=json.dumps({"username": username, "password": password}).encode(),
|
||||
|
||||
@@ -19,7 +19,7 @@ import time
|
||||
import urllib.request
|
||||
|
||||
checks = [
|
||||
("collector", "http://127.0.0.1:8081/healthz"),
|
||||
("collector", "http://127.0.0.1:8081/readyz"),
|
||||
("n8n", "http://127.0.0.1:5670/healthz"),
|
||||
("cli-proxy-api", "http://127.0.0.1:8317/v1/models"),
|
||||
]
|
||||
@@ -43,6 +43,6 @@ if pending:
|
||||
PY
|
||||
|
||||
echo "business started"
|
||||
echo "collector: http://127.0.0.1:8081/healthz"
|
||||
echo "collector: http://127.0.0.1:8081/readyz"
|
||||
echo "n8n: http://127.0.0.1:5670/healthz"
|
||||
echo "cli-proxy-api: http://127.0.0.1:8317/v1/models"
|
||||
|
||||
@@ -11,7 +11,7 @@ python3 - <<'PY'
|
||||
import urllib.request
|
||||
|
||||
for name, url in [
|
||||
("collector", "http://127.0.0.1:8081/healthz"),
|
||||
("collector", "http://127.0.0.1:8081/readyz"),
|
||||
("n8n", "http://127.0.0.1:5670/healthz"),
|
||||
("cli-proxy-api", "http://127.0.0.1:8317/v1/models"),
|
||||
]:
|
||||
|
||||
Reference in New Issue
Block a user