Skip to content

Commit 5ced904

Browse files
Pigbibicursoragent
andcommitted
refactor: extract strategy-switch worker into application modules
Move UX1, auth, RuntimeInstances, and shared HTTP helpers out of the worker monolith and update structural tests to match. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 9b97799 commit 5ced904

27 files changed

Lines changed: 19099 additions & 18174 deletions

‎tests/ux1_research_worker_validation.mjs‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -569,8 +569,15 @@ try {
569569

570570
assert.equal(__test.ux1CalculatorDeadlineMs, 30000);
571571
assert.equal(__test.ux1CalculatorMaxBytes, 65536);
572-
const workerSource = await readFile(new URL("../web/strategy-switch-console/worker.js", import.meta.url), "utf8");
573-
const previewFn = workerSource.slice(workerSource.indexOf("async function ux1PreviewPost"), workerSource.indexOf("async function ux1IntentPost"));
572+
// Implementation lives in application/ux1/research.js; worker.js only imports and routes.
573+
const ux1ResearchSource = await readFile(
574+
new URL("../web/strategy-switch-console/application/ux1/research.js", import.meta.url),
575+
"utf8",
576+
);
577+
const previewStart = ux1ResearchSource.indexOf("async function ux1PreviewPost");
578+
const intentStart = ux1ResearchSource.indexOf("async function ux1IntentPost");
579+
assert.equal(previewStart >= 0 && intentStart > previewStart, true);
580+
const previewFn = ux1ResearchSource.slice(previewStart, intentStart);
574581
assert.equal(previewFn.includes("readUx1CalculatorExchange(binding, canonicalJson(calculatorRequest))"), true);
575582
assert.equal(previewFn.includes("deadlineMs"), false);
576583
assert.equal(previewFn.includes("maxBytes"), false);
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
export const ACCOUNT_DIAGNOSIS_REQUEST_PATH = "/api/account-diagnosis";
2+
export const ACCOUNT_DIAGNOSIS_INTERNAL_PREFIX = "/api/internal/account-diagnosis/";
3+
export const ACCOUNT_DIAGNOSIS_TARGET_ID = "binance.crypto_live_pool_rotation";
4+
export const ACCOUNT_DIAGNOSIS_TARGETS = Object.freeze({
5+
[ACCOUNT_DIAGNOSIS_TARGET_ID]: Object.freeze({
6+
platform: "binance",
7+
recheckRepository: "QuantStrategyLab/BinancePlatform",
8+
recheckWorkflow: "runtime-target-lifecycle.yml",
9+
}),
10+
"longbridge.paper": Object.freeze({
11+
platform: "longbridge",
12+
recheckRepository: "QuantStrategyLab/LongBridgePlatform",
13+
recheckWorkflow: "runtime-target-lifecycle.yml",
14+
}),
15+
"longbridge.hk": Object.freeze({
16+
platform: "longbridge",
17+
recheckRepository: "QuantStrategyLab/LongBridgePlatform",
18+
recheckWorkflow: "runtime-target-lifecycle.yml",
19+
}),
20+
"longbridge.sg": Object.freeze({
21+
platform: "longbridge",
22+
recheckRepository: "QuantStrategyLab/LongBridgePlatform",
23+
recheckWorkflow: "runtime-target-lifecycle.yml",
24+
}),
25+
"schwab.primary": Object.freeze({
26+
platform: "schwab",
27+
recheckRepository: "QuantStrategyLab/CharlesSchwabPlatform",
28+
recheckWorkflow: "runtime-target-lifecycle.yml",
29+
}),
30+
"firstrade.primary": Object.freeze({
31+
platform: "firstrade",
32+
recheckRepository: "QuantStrategyLab/FirstradePlatform",
33+
recheckWorkflow: "runtime-target-lifecycle.yml",
34+
}),
35+
});
36+
export const ACCOUNT_DIAGNOSIS_TRIGGERS = ["incident", "manual_check"];
37+
export const ACCOUNT_DIAGNOSIS_STATUSES = ["queued", "running", "succeeded", "failed", "unknown"];
38+
export const ACCOUNT_DIAGNOSIS_REASON_CODES = ["diagnosis_ready", "capacity_unavailable", "codex_unavailable", "invalid_response"];
39+
export const ACCOUNT_DIAGNOSIS_MAX_BODY_BYTES = 16 * 1024;
40+
export const ACCOUNT_DIAGNOSIS_SUMMARY_MAX_CHARS = 600;
41+
export const ACCOUNT_DIAGNOSIS_DEDUPE_WINDOW_MS = 24 * 60 * 60 * 1000;
42+
export const ACCOUNT_DIAGNOSIS_AUTO_ENABLED_ENV = "ACCOUNT_DIAGNOSIS_AUTO_ENABLED";
43+
export const ACCOUNT_DIAGNOSIS_DO_ACTIONS = new Set([
44+
"diagnosis_create", "diagnosis_latest", "diagnosis_read", "diagnosis_mark_dispatch", "diagnosis_claim", "diagnosis_result", "diagnosis_mark_recheck", "diagnosis_native_intent", "diagnosis_native_save", "diagnosis_native_cancel",
45+
]);
46+
/** Stable 409/qualification reason codes for /api/account-diagnosis (P0-08). */
47+
export const ACCOUNT_DIAGNOSIS_CONFLICT_REASON_TEXT = Object.freeze({
48+
account_diagnosis_config_unavailable: "账户选项配置不可用,无法绑定诊断身份。",
49+
account_diagnosis_account_not_found: "未找到匹配的账户选项。",
50+
account_diagnosis_account_not_unique: "账户选项不唯一,无法安全绑定诊断身份。",
51+
account_diagnosis_target_not_admitted: "该 target 未纳入账户诊断准入清单。",
52+
account_diagnosis_identity_conflict: "target 与账户身份冲突或不唯一。",
53+
account_diagnosis_lifecycle_store_unavailable: "生命周期配置存储不可用。",
54+
account_diagnosis_lifecycle_match_missing: "缺少匹配的生命周期观测来源。",
55+
account_diagnosis_lifecycle_match_ambiguous: "生命周期观测来源不唯一。",
56+
account_diagnosis_source_unavailable: "生命周期来源不可用(非 ready/stale)。",
57+
account_diagnosis_observation_missing: "缺少观测时间,无法确认诊断资格。",
58+
account_diagnosis_incident_requires_attention: "incident 触发需要已确认的监测关注项。",
59+
account_diagnosis_native_binding_conflict: "诊断原生任务身份或材料绑定冲突。",
60+
account_diagnosis_workflow_binding_conflict: "诊断工作流运行绑定冲突。",
61+
account_diagnosis_schema_incompatible: "诊断上下文 schema 不兼容。",
62+
});
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { HttpError } from "../http_error.js";
2+
import { ACCOUNT_DIAGNOSIS_CONFLICT_REASON_TEXT } from "./constants.js";
3+
4+
export function accountDiagnosisConflict(reasonCode, status = 409) {
5+
const reason = ACCOUNT_DIAGNOSIS_CONFLICT_REASON_TEXT[reasonCode] || "";
6+
return new HttpError(reasonCode, status, { reason_code: reasonCode, reason });
7+
}
8+
export function accountDiagnosisErrorPayload(error) {
9+
const reason_code = String(error?.reason_code || error?.message || "account_diagnosis_unavailable");
10+
const payload = { ok: false, error: reason_code, reason_code };
11+
const reason = String(error?.reason || ACCOUNT_DIAGNOSIS_CONFLICT_REASON_TEXT[reason_code] || "").trim();
12+
if (reason) payload.reason = reason;
13+
return payload;
14+
}
15+
export function accountDiagnosisTaskFromRow(row) {
16+
if (!row) return null;
17+
return {
18+
request_id: row.request_id,
19+
platform: row.platform,
20+
key: row.account_key,
21+
target_id: row.target_id,
22+
trigger: row.trigger,
23+
observed_at: row.observed_at,
24+
checks: JSON.parse(row.checks_json),
25+
state_fingerprint: row.state_fingerprint,
26+
status: row.status,
27+
native: row.native_json ? JSON.parse(row.native_json) : null,
28+
backend_kind: row.native_json ? "native" : "legacy",
29+
workflow_run_id: row.workflow_run_id || null,
30+
workflow_run_attempt: row.workflow_run_attempt || null,
31+
job_id: row.job_id || null,
32+
summary: row.summary || "",
33+
reason_code: row.reason_code || "",
34+
dispatch_state: row.dispatch_state,
35+
recheck_dispatch_state: row.recheck_dispatch_state,
36+
auto_source: row.auto_source || "manual",
37+
created_at: row.created_at,
38+
updated_at: row.updated_at,
39+
};
40+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export const SESSION_COOKIE = "qsl_switch_session";
2+
export const OAUTH_STATE_COOKIE = "qsl_switch_oauth_state";
3+
export const SESSION_TTL_SECONDS = 8 * 60 * 60;
4+
export const AUTH_CONFIG_KEY = "auth_config";
5+
export const GITHUB_LOGIN_REQUEST_TIMEOUT_MS = 20000;
6+
export const GITHUB_API_TIMEOUT_MS = 8000;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
export function randomToken() {
2+
const bytes = new Uint8Array(24);
3+
crypto.getRandomValues(bytes);
4+
return base64UrlEncodeBytes(bytes);
5+
}
6+
7+
export function base64UrlEncodeJson(value) {
8+
return base64UrlEncodeBytes(new TextEncoder().encode(JSON.stringify(value)));
9+
}
10+
11+
export function base64UrlEncodeBytes(bytes) {
12+
let binary = "";
13+
for (const byte of bytes) binary += String.fromCharCode(byte);
14+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
15+
}
16+
17+
export function base64UrlDecode(value) {
18+
const base64 = value.replaceAll("-", "+").replaceAll("_", "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
19+
const binary = atob(base64);
20+
const bytes = new Uint8Array(binary.length);
21+
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
22+
return new TextDecoder().decode(bytes);
23+
}
24+
25+
export function parseCookies(header) {
26+
const result = {};
27+
for (const part of header.split(";")) {
28+
const [name, ...rest] = part.trim().split("=");
29+
if (!name) continue;
30+
result[name] = decodeURIComponent(rest.join("="));
31+
}
32+
return result;
33+
}
34+
35+
export function cookie(name, value, maxAge) {
36+
return `${name}=${encodeURIComponent(value)}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${maxAge}`;
37+
}
38+
39+
export function clearCookie(name) {
40+
return `${name}=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0`;
41+
}
42+
43+
export function clearOAuthCookie() {
44+
return { "Set-Cookie": clearCookie(OAUTH_STATE_COOKIE) };
45+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { GITHUB_API_TIMEOUT_MS } from "./constants.js";
2+
3+
export async function fetchWithTimeout(resource, init = {}, timeoutMs = GITHUB_API_TIMEOUT_MS, fetchImpl = fetch) {
4+
const controller = new AbortController();
5+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
6+
try {
7+
return await fetchImpl(resource, { ...init, signal: controller.signal });
8+
} catch (error) {
9+
if (error?.name === "AbortError") throw new Error("GitHub request timed out");
10+
throw error;
11+
} finally {
12+
clearTimeout(timeoutId);
13+
}
14+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { fetchWithTimeout } from "./fetch.js";
2+
import { GITHUB_API_TIMEOUT_MS } from "./constants.js";
3+
import { cleanGithubOrg, uniqueStrings } from "./identity_lists.js";
4+
5+
export function githubHeaders(token) {
6+
return {
7+
Authorization: `Bearer ${token}`,
8+
Accept: "application/vnd.github+json",
9+
"Content-Type": "application/json",
10+
"User-Agent": "QuantRuntimeSettings-StrategySwitchConsole",
11+
"X-GitHub-Api-Version": "2022-11-28",
12+
};
13+
}
14+
15+
export async function fetchGithubOrgLogins(token, timeoutMs = GITHUB_API_TIMEOUT_MS) {
16+
const orgs = [];
17+
for (let page = 1; page <= 5; page += 1) {
18+
const response = await fetchWithTimeout(`https://api.github.com/user/orgs?per_page=100&page=${page}`, {
19+
headers: githubHeaders(token),
20+
}, timeoutMs);
21+
if (!response.ok) return orgs;
22+
const payload = await response.json();
23+
if (!Array.isArray(payload) || !payload.length) break;
24+
for (const org of payload) {
25+
const login = cleanGithubOrg(org?.login || "", "github org");
26+
if (login) orgs.push(login);
27+
}
28+
if (payload.length < 100) break;
29+
}
30+
return uniqueStrings(orgs);
31+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
export function parseLoginList(value, fieldName) {
2+
return normalizeLoginList(value, fieldName);
3+
}
4+
5+
export function normalizeLoginList(value, fieldName) {
6+
const items = Array.isArray(value) ? value : String(value || "").split(/[\s,]+/);
7+
if (items.length > 80) throw new Error(`${fieldName} supports at most 80 logins`);
8+
return uniqueStrings(items.map((item) => cleanGithubLogin(item, fieldName)).filter(Boolean));
9+
}
10+
11+
export function parseOrgList(value, fieldName) {
12+
return normalizeOrgList(value, fieldName);
13+
}
14+
15+
export function normalizeOrgList(value, fieldName) {
16+
const items = Array.isArray(value) ? value : String(value || "").split(/[\s,]+/);
17+
if (items.length > 80) throw new Error(`${fieldName} supports at most 80 orgs`);
18+
return uniqueStrings(items.map((item) => cleanGithubOrg(item, fieldName)).filter(Boolean));
19+
}
20+
21+
export function cleanGithubLogin(value, fieldName) {
22+
const login = String(value || "").trim().toLowerCase();
23+
if (!login) return "";
24+
if (
25+
login.length > 39 ||
26+
!/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/.test(login) ||
27+
login.includes("--")
28+
) {
29+
throw new Error(`${fieldName} contains an invalid GitHub login`);
30+
}
31+
return login;
32+
}
33+
34+
export function cleanGithubOrg(value, fieldName) {
35+
return cleanGithubLogin(value, fieldName);
36+
}
37+
38+
export function uniqueStrings(items) {
39+
const result = [];
40+
const seen = new Set();
41+
for (const item of items) {
42+
const text = String(item || "").trim().toLowerCase();
43+
if (!text || seen.has(text)) continue;
44+
seen.add(text);
45+
result.push(text);
46+
}
47+
return result;
48+
}

0 commit comments

Comments
 (0)