diff --git a/apps/backend/src/services/admin/platform-config/cloudtentacles-service.ts b/apps/backend/src/services/admin/platform-config/cloudtentacles-service.ts index 83a97f97..3d5d92b5 100644 --- a/apps/backend/src/services/admin/platform-config/cloudtentacles-service.ts +++ b/apps/backend/src/services/admin/platform-config/cloudtentacles-service.ts @@ -196,8 +196,8 @@ export async function validateAdminCloudtentaclesSession( payload: AdminCloudtentaclesValidateSessionInput = {} ) { const sourceKey = String(payload.sourceKey || "").trim() || "default"; - const savedSource = getCloudtentaclesSourceByKey(sourceKey) || {}; - const persistedSession = getCloudtentaclesSessionStateByKey(sourceKey) || {}; + const savedSource: JsonObject = getCloudtentaclesSourceByKey(sourceKey) || {}; + const persistedSession: JsonObject = getCloudtentaclesSessionStateByKey(sourceKey) || {}; const sessionContext = resolveAdminCloudtentaclesSessionPayload(payload, { savedSource, persistedSession, diff --git a/apps/backend/src/services/platforms/cloudtentacles/crypto-service.js b/apps/backend/src/services/platforms/cloudtentacles/crypto-service.ts similarity index 87% rename from apps/backend/src/services/platforms/cloudtentacles/crypto-service.js rename to apps/backend/src/services/platforms/cloudtentacles/crypto-service.ts index 9c5af2ca..5c905738 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/crypto-service.js +++ b/apps/backend/src/services/platforms/cloudtentacles/crypto-service.ts @@ -1,15 +1,15 @@ -// @ts-check - import { createHash, constants, publicEncrypt } from 'node:crypto' import { createHttpError } from '../../../utils/http.js' import { resolveCloudtentaclesConfig } from './shared.js' -export function md5CloudtentaclesPassword(password) { +type JsonObject = Record + +export function md5CloudtentaclesPassword(password: unknown) { return createHash('md5').update(String(password || ''), 'utf8').digest('hex') } -export function encryptCloudtentaclesPayload(payload = {}, options = {}) { +export function encryptCloudtentaclesPayload(payload: JsonObject = {}, options: JsonObject = {}) { const config = resolveCloudtentaclesConfig(options) const publicKeyPem = String(config.publicKeyPem || '').trim() @@ -54,7 +54,7 @@ export function encryptCloudtentaclesPayload(payload = {}, options = {}) { } } -function removeEmptyFields(payload) { +function removeEmptyFields(payload: unknown) { if (!payload || typeof payload !== 'object') { return {} } diff --git a/apps/backend/src/services/platforms/cloudtentacles/session-state-service.js b/apps/backend/src/services/platforms/cloudtentacles/session-state-service.ts similarity index 87% rename from apps/backend/src/services/platforms/cloudtentacles/session-state-service.js rename to apps/backend/src/services/platforms/cloudtentacles/session-state-service.ts index 4e7a447c..b5c3e343 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/session-state-service.js +++ b/apps/backend/src/services/platforms/cloudtentacles/session-state-service.ts @@ -1,5 +1,3 @@ -// @ts-check - import fs from 'node:fs' import path from 'node:path' @@ -7,6 +5,8 @@ import { PROJECT_ROOT } from '../../../config/runtime.js' const CLOUDTENTACLES_SESSION_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'cloudtentacles-session.json') +type JsonObject = Record + export function getCloudtentaclesSessionFilePath() { return CLOUDTENTACLES_SESSION_FILE_PATH } @@ -23,7 +23,7 @@ export function getCloudtentaclesSessionState() { /** * Get a session by its sourceKey. Returns null if not found. */ -export function getCloudtentaclesSessionStateByKey(sourceKey) { +export function getCloudtentaclesSessionStateByKey(sourceKey: unknown) { const states = loadCloudtentaclesSessionStatesFromFile() const key = String(sourceKey || '').trim() if (!key) return null @@ -41,7 +41,7 @@ export function getAllCloudtentaclesSessionStates() { * Backward-compatible save: accepts both old single-object format * and new sessions-map format, normalizes, and persists. */ -export function saveCloudtentaclesSessionState(rawValue) { +export function saveCloudtentaclesSessionState(rawValue: unknown) { const normalized = normalizeSessionStatesFile(rawValue) fs.mkdirSync(path.dirname(CLOUDTENTACLES_SESSION_FILE_PATH), { recursive: true }) fs.writeFileSync(CLOUDTENTACLES_SESSION_FILE_PATH, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8') @@ -51,7 +51,7 @@ export function saveCloudtentaclesSessionState(rawValue) { /** * Save a session for a specific sourceKey. */ -export function saveCloudtentaclesSessionStateByKey(sourceKey, rawValue) { +export function saveCloudtentaclesSessionStateByKey(sourceKey: unknown, rawValue: unknown) { const key = String(sourceKey || '').trim() if (!key) { throw new Error('saveCloudtentaclesSessionStateByKey: sourceKey is required') @@ -75,7 +75,7 @@ export function clearCloudtentaclesSessionState() { /** * Clear a session by sourceKey (sets it to default empty state). */ -export function clearCloudtentaclesSessionStateByKey(sourceKey) { +export function clearCloudtentaclesSessionStateByKey(sourceKey: unknown) { const key = String(sourceKey || '').trim() if (!key) { throw new Error('clearCloudtentaclesSessionStateByKey: sourceKey is required') @@ -106,7 +106,7 @@ function loadCloudtentaclesSessionStatesFromFile() { /** * Normalize a single session state item. */ -function normalizeCloudtentaclesSessionState(rawValue) { +function normalizeCloudtentaclesSessionState(rawValue: unknown) { const source = isPlainObject(rawValue) ? rawValue : {} return { @@ -125,7 +125,7 @@ function normalizeCloudtentaclesSessionState(rawValue) { * Handles old format (single object without sessions key) by auto-wrapping * into { sessions: { 'default': ... } }. */ -function normalizeSessionStatesFile(rawValue) { +function normalizeSessionStatesFile(rawValue: unknown) { // Old format: { token: 'xxx', ... } (single object, no sessions key) if (isPlainObject(rawValue) && !rawValue.sessions) { return { @@ -137,7 +137,7 @@ function normalizeSessionStatesFile(rawValue) { // New format: { sessions: { 'default': {...}, ... } } return { - sessions: isPlainObject(rawValue?.sessions) + sessions: isPlainObject(rawValue) && isPlainObject(rawValue.sessions) ? Object.fromEntries( Object.entries(rawValue.sessions).map(([k, v]) => [k, normalizeCloudtentaclesSessionState(v)]) ) @@ -165,11 +165,11 @@ function createDefaultCloudtentaclesSessionStates() { } } -function normalizeInteger(value, fallback) { +function normalizeInteger(value: unknown, fallback: number) { const parsed = Number(value) return Number.isInteger(parsed) ? parsed : fallback } -function isPlainObject(value) { +function isPlainObject(value: unknown): value is JsonObject { return Object.prototype.toString.call(value) === '[object Object]' } diff --git a/apps/backend/src/services/platforms/cloudtentacles/shared.js b/apps/backend/src/services/platforms/cloudtentacles/shared.ts similarity index 89% rename from apps/backend/src/services/platforms/cloudtentacles/shared.js rename to apps/backend/src/services/platforms/cloudtentacles/shared.ts index 9903b61c..3676db98 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/shared.js +++ b/apps/backend/src/services/platforms/cloudtentacles/shared.ts @@ -1,11 +1,11 @@ -// @ts-check - import { runtimeConfig } from '../../../config/runtime.js' -/** @typedef {import('../../../types/runtime-config.js').RuntimeConfig} RuntimeConfig */ +import type { RuntimeConfig } from '../../../types/runtime-config.js' -export function resolveCloudtentaclesConfig(overrides = {}) { - /** @type {RuntimeConfig['platforms']['cloudtentacles']} */ +type CloudtentaclesRuntimeConfig = RuntimeConfig['platforms']['cloudtentacles'] +type JsonObject = Record + +export function resolveCloudtentaclesConfig(overrides: Partial = {}) { const baseConfig = runtimeConfig.platforms?.cloudtentacles || { baseUrl: '', timeoutMs: 5000, @@ -92,7 +92,11 @@ export function resolveCloudtentaclesConfig(overrides = {}) { } } -export function buildCloudtentaclesUrl(baseUrl, pathname, searchParams = null) { +export function buildCloudtentaclesUrl( + baseUrl: unknown, + pathname: unknown, + searchParams: JsonObject | null = null, +) { const url = new URL(normalizePath(pathname, '/'), normalizeBaseUrl(baseUrl) || 'https://123.207.217.176') if (searchParams && typeof searchParams === 'object') { @@ -113,8 +117,14 @@ export function buildCloudtentaclesHeaders({ deviceId = '-', deviceType = 0, extra = {}, +}: { + token?: unknown + contentType?: string + deviceId?: unknown + deviceType?: unknown + extra?: JsonObject } = {}) { - const headers = { + const headers: JsonObject = { accept: 'application/json, text/plain, */*', ...(contentType ? { 'content-type': contentType } : {}), deviceid: String(deviceId || '-').trim() || '-', @@ -130,11 +140,11 @@ export function buildCloudtentaclesHeaders({ return headers } -function normalizeBaseUrl(value) { +function normalizeBaseUrl(value: unknown) { return String(value || '').trim().replace(/\/+$/, '') } -function normalizePath(value, fallback) { +function normalizePath(value: unknown, fallback: string) { const normalized = String(value || '').trim() if (!normalized) { return fallback @@ -143,17 +153,17 @@ function normalizePath(value, fallback) { return normalized.startsWith('/') ? normalized : `/${normalized}` } -function normalizePositiveInteger(value, fallback) { +function normalizePositiveInteger(value: unknown, fallback: number) { const parsed = Number(value) return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback } -function normalizeInteger(value, fallback) { +function normalizeInteger(value: unknown, fallback: number) { const parsed = Number(value) return Number.isInteger(parsed) ? parsed : fallback } -function normalizeHeaderMap(extra) { +function normalizeHeaderMap(extra: unknown) { if (!extra || typeof extra !== 'object') { return {} } @@ -165,7 +175,7 @@ function normalizeHeaderMap(extra) { ) } -function normalizePem(value) { +function normalizePem(value: unknown) { return String(value || '') .replace(/\r/g, '') .split('\n') diff --git a/apps/backend/src/services/platforms/cloudtentacles/source-config-service.js b/apps/backend/src/services/platforms/cloudtentacles/source-config-service.ts similarity index 89% rename from apps/backend/src/services/platforms/cloudtentacles/source-config-service.js rename to apps/backend/src/services/platforms/cloudtentacles/source-config-service.ts index 5cce28fd..b5ef8faa 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/source-config-service.js +++ b/apps/backend/src/services/platforms/cloudtentacles/source-config-service.ts @@ -1,5 +1,3 @@ -// @ts-check - import fs from 'node:fs' import path from 'node:path' @@ -7,6 +5,8 @@ import { PROJECT_ROOT } from '../../../config/runtime.js' const CLOUDTENTACLES_SOURCES_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'cloudtentacles-sources.json') +type JsonObject = Record + export function getCloudtentaclesSourcesFilePath() { return CLOUDTENTACLES_SOURCES_FILE_PATH } @@ -24,7 +24,7 @@ export function getCloudtentaclesSourceConfig() { /** * Get a source item by its key. Returns null if not found. */ -export function getCloudtentaclesSourceByKey(sourceKey) { +export function getCloudtentaclesSourceByKey(sourceKey: unknown) { const config = loadCloudtentaclesSourcesConfigFromFile() const key = String(sourceKey || '').trim() if (!key) return null @@ -42,7 +42,7 @@ export function listCloudtentaclesSources() { * Backward-compatible save: accepts both old single-object format * and new list format, normalizes, and persists. */ -export function saveCloudtentaclesSourceConfig(rawValue) { +export function saveCloudtentaclesSourceConfig(rawValue: unknown) { const normalized = normalizeCloudtentaclesSourcesConfig(rawValue) fs.mkdirSync(path.dirname(CLOUDTENTACLES_SOURCES_FILE_PATH), { recursive: true }) fs.writeFileSync(CLOUDTENTACLES_SOURCES_FILE_PATH, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8') @@ -52,7 +52,7 @@ export function saveCloudtentaclesSourceConfig(rawValue) { /** * Save the entire list-format config object: { enabled, sources: [...] }. */ -export function saveCloudtentaclesSourcesList(rawValue) { +export function saveCloudtentaclesSourcesList(rawValue: unknown) { return saveCloudtentaclesSourceConfig(rawValue) } @@ -60,7 +60,7 @@ export function saveCloudtentaclesSourcesList(rawValue) { * Save or update a single source item identified by sourceKey. * If a source with the same key exists, it is replaced; otherwise it is appended. */ -export function saveCloudtentaclesSourceByKey(sourceKey, data) { +export function saveCloudtentaclesSourceByKey(sourceKey: unknown, data: JsonObject = {}) { const key = String(sourceKey || '').trim() if (!key) { throw new Error('saveCloudtentaclesSourceByKey: sourceKey is required') @@ -84,7 +84,7 @@ export function saveCloudtentaclesSourceByKey(sourceKey, data) { /** * Delete a single source by key. Throws if key is 'default' (cannot delete default source). */ -export function deleteCloudtentaclesSourceByKey(sourceKey) { +export function deleteCloudtentaclesSourceByKey(sourceKey: unknown) { const key = String(sourceKey || '').trim() if (!key) { throw new Error('deleteCloudtentaclesSourceByKey: sourceKey is required') @@ -127,7 +127,7 @@ function loadCloudtentaclesSourcesConfigFromFile() { /** * Normalize a single source item. Adds key (required) and label (optional). */ -function normalizeCloudtentaclesSourceItem(rawValue) { +function normalizeCloudtentaclesSourceItem(rawValue: unknown) { const source = isPlainObject(rawValue) ? rawValue : {} return { @@ -146,7 +146,7 @@ function normalizeCloudtentaclesSourceItem(rawValue) { * Normalize the overall config. Handles both old single-object format * (auto-migrates to new list format) and new { enabled, sources } format. */ -function normalizeCloudtentaclesSourcesConfig(rawValue) { +function normalizeCloudtentaclesSourcesConfig(rawValue: unknown) { // Old format: { enabled: true, username: 'xxx', ... } (single object, no sources array) if (isPlainObject(rawValue) && !Array.isArray(rawValue.sources)) { return { @@ -179,11 +179,11 @@ function createDefaultCloudtentaclesSourcesConfig() { } } -function normalizeInteger(value, fallback) { +function normalizeInteger(value: unknown, fallback: number) { const parsed = Number(value) return Number.isInteger(parsed) ? parsed : fallback } -function isPlainObject(value) { +function isPlainObject(value: unknown): value is JsonObject { return Object.prototype.toString.call(value) === '[object Object]' } diff --git a/docs/backend-typescript-migration-plan.md b/docs/backend-typescript-migration-plan.md index fbea867d..ab017405 100644 --- a/docs/backend-typescript-migration-plan.md +++ b/docs/backend-typescript-migration-plan.md @@ -636,6 +636,17 @@ - `npm run typecheck` - `npm run build` - `npm test` 共 139 个用例通过 +153. Cloudtentacles 平台底座模块迁移到 `.ts`: + - `src/services/platforms/cloudtentacles/shared.ts` + - `src/services/platforms/cloudtentacles/source-config-service.ts` + - `src/services/platforms/cloudtentacles/session-state-service.ts` + - `src/services/platforms/cloudtentacles/crypto-service.ts` +154. Cloudtentacles 运行时配置解析、URL / headers 构造、来源配置读写、会话状态读写、RSA 加密入口已进入 TS 编译链路;动态配置、会话 map、加密 payload 与 header map 补齐类型 +155. Docker 内验证通过: + - `src/services/admin/platform-config/*.test.js` 共 37 个用例通过 + - `npm run typecheck` + - `npm run build` + - `npm test` 共 139 个用例通过 ## 下一步建议